Closed puntiz closed 2 months ago
Thanks, it's nice to hear that you like my package and appreciate my work, that means a lot to me :)
I never tested it in a real-time case, but I think it should be possible. Below is the example with two widgets that should help you realize this project.
Btw, I will move your question to the Discussions tab now because I prefer that tab for questions.
// Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// Package imports:
import 'package:pro_image_editor/pro_image_editor.dart';
// ignore: constant_identifier_names
const DEMO_IMAGE = 'https://picsum.photos/id/230/2000';
class RealtimeViewerExample extends StatefulWidget {
const RealtimeViewerExample({super.key});
@override
State<RealtimeViewerExample> createState() => _RealtimeViewerExampleState();
}
class _RealtimeViewerExampleState extends State<RealtimeViewerExample> {
final _editorKey = GlobalKey<ProImageEditorState>();
@override
void initState() {
_connectToRealtimeEvents();
super.initState();
}
@override
void dispose() {
// TODO: Don't forget to close your realtime connection
super.dispose();
}
void _connectToRealtimeEvents() {
String json = '{}';
/// TODO: Listen to realtime changes and import it like below.
if (kDebugMode) debugPrint('Import changes');
_editorKey.currentState?.importStateHistory(
ImportStateHistory.fromJson(
json,
// Optinal configs
configs: const ImportEditorConfigs(
recalculateSizeAndPosition: true,
mergeMode: ImportEditorMergeMode.replace,
),
),
);
}
void _openViewerEditor() async {
await precacheImage(const NetworkImage(DEMO_IMAGE), context);
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _buildEditor(),
),
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: _openViewerEditor,
leading: const Icon(Icons.smart_display_outlined),
title: const Text('Realtime-Viewer'),
trailing: const Icon(Icons.chevron_right),
);
}
Widget _buildEditor() {
/// There can be problems if the aspect ratio is not the same as in the
/// presenter view, so I recommend that you make sure the editor has the
/// same aspect ratio as the presenter.
return ProImageEditor.network(
DEMO_IMAGE,
key: _editorKey,
callbacks: const ProImageEditorCallbacks(),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
customWidgets: ImageEditorCustomWidgets(
mainEditor: CustomWidgetsMainEditor(
appBar: (editor, rebuildStream) => ReactiveCustomAppbar(
stream: rebuildStream,
builder: (_) => AppBar(
title: const Text('Realtime-Viewer'),
),
),
/// I hide the bottom bar in the viewer because in the viewer it's
/// not needed for editing.
bottomBar: (editor, rebuildStream, key) => ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => const SizedBox.shrink(),
),
/// Wrap the full body content inside `IgnorePointer` that the user
/// can't move layers. Optionally is it maybe also usefull when your
/// users can zoom so you can also wrap it with `InteractiveViewer`.
wrapBody: (editor, rebuildStream, content) {
return IgnorePointer(
child: content,
);
},
/// Optionally add elements to the body. For example, show a
/// progress-spinner each time changes are made.
/// bodyItems: (editor, rebuildStream) => [
/// ReactiveCustomWidget(
/// builder: (_) => const CircularProgressIndicator(),
/// stream: rebuildStream,
/// ),
/// ],
),
),
/// Optionally change the background color like below
imageEditorTheme: ImageEditorTheme(
background: Colors.blue.shade100,
)
/// Optionally open the editor directly with a state history from the
/// presenter like below
/// stateHistoryConfigs: StateHistoryConfigs(
/// initStateHistory: ImportStateHistory.fromJson(json),
/// ),
),
);
}
}
class RealtimePresenterExample extends StatefulWidget {
const RealtimePresenterExample({super.key});
@override
State<RealtimePresenterExample> createState() =>
_RealtimePresenterExampleState();
}
class _RealtimePresenterExampleState extends State<RealtimePresenterExample> {
final _editorKey = GlobalKey<ProImageEditorState>();
void _sendRealtimeEvents() async {
if (kDebugMode) debugPrint('Start export');
var export = await _editorKey.currentState?.exportStateHistory(
configs: const ExportEditorConfigs(
/// Send only the current view
historySpan: ExportHistorySpan.current,
/// Optionally disable some things from being sent. Keep in mind that
/// sending stickers will slow down the export progress because the
/// editor has to convert them to png images first.
exportSticker: true,
),
);
var sendJson = await export?.toJson();
/// TODO: Send your JSON `sendJson` over your real-time connection to the viewers.
if (kDebugMode) debugPrint('Done export');
}
void _openPresenterEditor() async {
await precacheImage(const NetworkImage(DEMO_IMAGE), context);
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _buildEditor(),
),
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: _openPresenterEditor,
leading: const Icon(Icons.present_to_all_outlined),
title: const Text('Realtime-Presenter'),
trailing: const Icon(Icons.chevron_right),
);
}
Widget _buildEditor() {
/// There can be problems if the aspect ratio is not the same as in the
/// presenter view, so I recommend that you make sure the editor has the
/// same aspect ratio as the presenter.
return ProImageEditor.network(
DEMO_IMAGE,
key: _editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingComplete: (bytes) async {
/// Handle the final image => Maybe upload it to your server.
},
/// Optionally send changes directly when changes occur. Below in
/// `customWidgets` you can see the example of how to add a button to
/// send manually for more control for the presenter.
/// mainEditorCallbacks: MainEditorCallbacks(
/// onAddLayer: (value) => _sendRealtimeEvents(),
/// onUpdateLayer: (value) => _sendRealtimeEvents(),
/// onUndo: _sendRealtimeEvents,
/// onRedo: _sendRealtimeEvents,
/// ),
/// paintEditorCallbacks: PaintEditorCallbacks(
/// onDrawingDone: _sendRealtimeEvents,
/// onUndo: _sendRealtimeEvents,
/// onRedo: _sendRealtimeEvents,
/// ),
/// More editor callbacks...
),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
/// Update the appbar with a button that the presenter can send it to the
/// viewers
customWidgets: ImageEditorCustomWidgets(
mainEditor: CustomWidgetsMainEditor(
appBar: (editor, rebuildStream) => editor.selectedLayerIndex < 0
? ReactiveCustomAppbar(
stream: rebuildStream,
builder: (_) => _buildCustomAppBar(editor),
)
: null,
),
),
),
);
}
AppBar _buildCustomAppBar(ProImageEditorState editor) {
return AppBar(
automaticallyImplyLeading: false,
foregroundColor: Colors.white,
backgroundColor: Colors.black,
actions: [
IconButton(
tooltip: 'Cancel',
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: const Icon(Icons.close),
onPressed: editor.closeEditor,
),
const Spacer(),
/// Create your custom button to send realtime events to the viewers
IconButton(
tooltip: 'Send to viewers',
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: const Icon(Icons.send),
onPressed: _sendRealtimeEvents,
),
const Spacer(),
IconButton(
tooltip: 'Undo',
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: Icon(
Icons.undo,
color: editor.canUndo == true
? Colors.white
: Colors.white.withAlpha(80),
),
onPressed: editor.undoAction,
),
IconButton(
tooltip: 'Redo',
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: Icon(
Icons.redo,
color: editor.canRedo == true
? Colors.white
: Colors.white.withAlpha(80),
),
onPressed: editor.redoAction,
),
IconButton(
tooltip: 'Done',
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: const Icon(Icons.done),
iconSize: 28,
onPressed: editor.doneEditing,
),
],
);
}
}
Hey, love the package and really appreciate the work you have put in.
Question, Is there a way to build a viewer version with primary purpose being to import changes JSON and display them.
I want to use this package to allow presenter to make changes and for those to, on a realtime basis, reflect on participants screens. Exporting an image in such a case is not feasible.
Would love to know your thoughts.