flutter_deck icon indicating copy to clipboard operation
flutter_deck copied to clipboard

feat: Multi-window presenter mode

Open aloisdeniel opened this issue 6 months ago • 2 comments

Description

It would be awesome to have a simple multi window plugin for the presenter mode!

When starting the app on desktop (not web), it would simply open a second window in presenter mode.

This package package might help: desktop_multi_window

aloisdeniel avatar Jun 21 '25 21:06 aloisdeniel

@aloisdeniel Hey, yeah, makes total sense. I was trying to postpone using any plugin for this, considering that native Flutter multi-window support was coming for desktop soon, but... I said this more than a year ago already, I suppose 😄 For now, flutter_deck supports this out of the box for Web only.

mkobuolys avatar Jun 22 '25 19:06 mkobuolys

In case someone wants a macOS presenter window here is a quick implementation:

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
// ignore: depend_on_referenced_packages
import 'package:flutter_deck_client/flutter_deck_client.dart';

class PresenterClient implements FlutterDeckClient {
  PresenterClient(this.windowId);
  final int windowId;
  final StreamController<FlutterDeckState> _stateController =
      StreamController<FlutterDeckState>.broadcast();
  AppLifecycleListener? _appLifecycleListener;

  static Future<({bool isPresenter, int windowId})> open(
    List<String> args,
  ) async {
    if (args.firstOrNull == 'multi_window') {
      return (isPresenter: true, windowId: 0);
    }

    final window = await DesktopMultiWindow.createWindow(jsonEncode({}));
    window
      ..setFrame(const Offset(0, 0) & const Size(880, 520))
      ..center()
      ..setTitle('Presenter')
      ..show();
    return (isPresenter: false, windowId: window.windowId);
  }

  @override
  Stream<FlutterDeckState> get flutterDeckStateStream =>
      _stateController.stream;

  @override
  void dispose() {
    _stateController.close();
    DesktopMultiWindow.setMethodHandler(null);
    _appLifecycleListener?.dispose();
    _appLifecycleListener = null;
  }

  @override
  void init([FlutterDeckState? state]) async {
    DesktopMultiWindow.setMethodHandler(_handleMethodCallback);

    // Workaround: https://github.com/MixinNetwork/flutter-plugins/issues/319
    if (windowId != 0 && Platform.isMacOS) {
      _appLifecycleListener = AppLifecycleListener(
        onHide:
            // ignore: invalid_use_of_protected_member
            () => SchedulerBinding.instance.handleAppLifecycleStateChanged(
              AppLifecycleState.inactive,
            ),
      );
    }
    if (state == null) {
      return;
    }
    updateState(state);
  }

  Future<dynamic> _handleMethodCallback(
    MethodCall call,
    int fromWindowId,
  ) async {
    if (call.method == 'updateState') {
      _stateController.add(
        FlutterDeckState.fromJson(jsonDecode(call.arguments)),
      );
    }
  }

  @override
  void updateState(FlutterDeckState state) {
    DesktopMultiWindow.invokeMethod(
      windowId,
      'updateState',
      jsonEncode(state.toJson()),
    );
  }
}

And usage:

void main(List<String> args) async {
  final presenter = await PresenterClient.open(args);

    return FlutterDeckApp(
      isPresenterView: widget.isPresenter,
      client: PresenterClient(widget.windowId),
      // ...

aloisdeniel avatar Jun 23 '25 19:06 aloisdeniel