hm21 / pro_image_editor

The pro_image_editor is a Flutter widget designed for image editing within your application. It provides a flexible and convenient way to integrate image editing capabilities into your Flutter project.
https://hm21.github.io/pro_image_editor/
BSD 3-Clause "New" or "Revised" License
93 stars 58 forks source link

Pro Image Viewer #137

Closed puntiz closed 2 months ago

puntiz commented 2 months ago

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.

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