multi_screen_presentation 1.0.9 copy "multi_screen_presentation: ^1.0.9" to clipboard
multi_screen_presentation: ^1.0.9 copied to clipboard

A comprehensive Flutter plugin to manage presentation windows on secondary screens with customizable properties.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:multi_screen_presentation/multi_screen_presentation.dart';
import 'package:device_info_plus/device_info_plus.dart';

// =========================================================================
// 1. DEFINITION DES PAGES / SLIDES
// =========================================================================

enum AppRoute {
  accueil,
  presentation,
  statistiques,
}

class SharedPageContent extends StatelessWidget {
  final AppRoute route;

  const SharedPageContent({super.key, required this.route});

  @override
  Widget build(BuildContext context) {
    switch (route) {
      case AppRoute.accueil:
        return const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.home, size: 80, color: Colors.blue),
              SizedBox(height: 16),
              Text("ÉCRAN D'ACCUEIL", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
              Text("Bienvenue dans l'application", style: TextStyle(color: Colors.grey)),
            ],
          ),
        );
      case AppRoute.presentation:
        return const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.slideshow, size: 80, color: Colors.cyan),
              SizedBox(height: 16),
              Text("PAGE DE PRÉSENTATION", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
              Text("Contenu synchronisé instantanément !", style: TextStyle(color: Colors.grey)),
            ],
          ),
        );
      case AppRoute.statistiques:
        // On appelle ici un vrai plugin tiers (device_info_plus) depuis
        // cette page, qu'elle soit affichée dans la fenêtre principale
        // (aperçu) ou dans le moteur Flutter secondaire créé par notre
        // plugin natif. C'est le test de bout en bout : si l'enregistrement
        // des plugins sur la 2e fenêtre est cassé, on verra une
        // MissingPluginException s'afficher ci-dessous au lieu des infos.
        return const _DeviceInfoStatsView();
    }
  }
}

class _DeviceInfoStatsView extends StatefulWidget {
  const _DeviceInfoStatsView();

  @override
  State<_DeviceInfoStatsView> createState() => _DeviceInfoStatsViewState();
}

class _DeviceInfoStatsViewState extends State<_DeviceInfoStatsView> {
  String? _summary;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadDeviceInfo();
  }

  Future<void> _loadDeviceInfo() async {
    final deviceInfoPlugin = DeviceInfoPlugin();

    try {
      if (Platform.isWindows) {
        // Ce qui s'exécute sur Windows
        final windowsInfo = await deviceInfoPlugin.windowsInfo;
        setState(() {
          _summary = "PC : ${windowsInfo.computerName}";
        });
      } else if (Platform.isMacOS) {
        // Ce qui doit s'exécuter sur votre Mac !
        final macInfo = await deviceInfoPlugin.macOsInfo;
        setState(() {
          _summary = "Mac : ${macInfo.computerName} (${macInfo.model})";
        });
      } else if (Platform.isLinux) {
        // Ce qui s'exécute sur Linux !
        final linuxInfo = await deviceInfoPlugin.linuxInfo;
        setState(() {
          _summary = "Linux : ${linuxInfo.prettyName}";
        });
      } else if (Platform.isAndroid) {
        // Ce qui s'exécute sur Android !
        final androidInfo = await deviceInfoPlugin.androidInfo;
        setState(() {
          _summary = "Android : ${androidInfo.brand} ${androidInfo.model} (SDK ${androidInfo.version.sdkInt})";
        });
      } else if (Platform.isIOS) {
        // Ce qui s'exécute sur iOS !
        final iosInfo = await deviceInfoPlugin.iosInfo;
        setState(() {
          _summary = "iOS : ${iosInfo.name} (${iosInfo.model} ${iosInfo.systemVersion})";
        });
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.bar_chart, size: 80, color: Colors.green),
          const SizedBox(height: 16),
          const Text("TABLEAU DE BORD", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          if (_error != null)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 24),
              child: Text(
                "Échec du plugin device_info_plus :\n$_error",
                textAlign: TextAlign.center,
                style: const TextStyle(color: Colors.red),
              ),
            )
          else if (_summary != null)
            Text(_summary!, textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey))
          else
            const CircularProgressIndicator(),
        ],
      ),
    );
  }
}

// =========================================================================
// 2. ENTRYPOINT DE LA FENÊTRE SECONDAIRE / RETOUR
// =========================================================================

@pragma('vm:entry-point')
void presentationMain() {
  runApp(const PresentationApp());
}

class PresentationApp extends StatelessWidget {
  const PresentationApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: PresentationPage(),
    );
  }
}

class PresentationPage extends StatefulWidget {
  const PresentationPage({super.key});

  @override
  State<PresentationPage> createState() => _PresentationPageState();
}

class _PresentationPageState extends State<PresentationPage> {
  AppRoute _currentRoute = AppRoute.accueil;

  @override
  void initState() {
    super.initState();
    
    // Écoute des données transmises via le Stream natif du package
    WindowManager.onDataReceived.listen((Map<String, dynamic> data) {
      if (data.containsKey('route')) {
        setState(() {
          _currentRoute = AppRoute.values.firstWhere(
            (r) => r.name == data['route'],
            orElse: () => AppRoute.accueil,
          );
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SharedPageContent(route: _currentRoute),
    );
  }
}

// =========================================================================
// 3. APPLICATION PRINCIPALE (LA TÉLÉCOMMANDE)
// =========================================================================

void main(List<String> args) {
  // Sur Linux, il n'existe pas d'API pour lancer un FlView sur un
  // entrypoint Dart nommé différent de main() (limitation du moteur
  // Flutter, cf. flutter/flutter#86599). Le plugin transmet donc
  // l'entrypoint voulu via les arguments de ligne de commande de l'isolate
  // (--entrypoint <nom>), et c'est à nous de dispatcher manuellement ici.
  final entryIndex = args.indexOf('--entrypoint');
  final entrypoint = (entryIndex != -1 && entryIndex + 1 < args.length)
      ? args[entryIndex + 1]
      : null;

  if (entrypoint == 'presentationMain') {
    presentationMain();
    return;
  }

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<ScreenInfo> _screens = [];

  // Une seule fenêtre active par écran pour éviter les doublons
  final Map<String, Window> _activeWindows = {};
  final Map<String, bool> _screenFullscreenChoices = {};

  final TextEditingController _titleController =
      TextEditingController(text: 'Écran de Présentation');
  final TextEditingController _iconPathController = TextEditingController();
  final TextEditingController _xController = TextEditingController(text: '100');
  final TextEditingController _yController = TextEditingController(text: '100');
  final TextEditingController _widthController =
      TextEditingController(text: '1280');
  final TextEditingController _heightController =
      TextEditingController(text: '720');

  Window? _previewWindow;
  AppRoute _activeRoute = AppRoute.accueil;
  double _windowOpacity = 1.0;
  bool _windowAlwaysOnTop = false;
  bool _windowResizable = true;
  bool _windowVisible = true;
  bool _windowFullscreen = false;

  @override
  void initState() {
    super.initState();
    _refreshScreens();
    WindowManager.onScreensChanged.listen((s) => setState(() => _screens = s));
  }

  @override
  void dispose() {
    _titleController.dispose();
    _iconPathController.dispose();
    _xController.dispose();
    _yController.dispose();
    _widthController.dispose();
    _heightController.dispose();
    super.dispose();
  }

  Future<void> _refreshScreens() async {
    final screens = await WindowManager.getScreens();
    setState(() {
      _screens = screens;
      for (var s in screens) {
        _screenFullscreenChoices.putIfAbsent(s.id, () => false);
      }
    });
  }

  /// Ouvre ou ferme une fenêtre unique de manière exclusive pour cet écran
  /// Ouvre ou ferme une fenêtre unique de manière exclusive pour cet écran
  Future<void> _toggleScreen(ScreenInfo screen) async {
    if (_activeWindows.containsKey(screen.id)) {
      await _activeWindows[screen.id]!.close();
      setState(() {
        _activeWindows.remove(screen.id);
      });
      return;
    }

    final forceFullscreen = _screenFullscreenChoices[screen.id] ?? false;
    final title = _titleController.text.trim().isEmpty
        ? 'Écran de Présentation'
        : _titleController.text.trim();
    final iconPath = _iconPathController.text.trim().isEmpty
        ? null
        : _iconPathController.text.trim();

    Window? window;
    if(forceFullscreen) {
      window = await WindowManager.openWindow(
        WindowOptions(
          screenId: screen.id,
          fullscreen: forceFullscreen,
          contentMode: PresentationContentMode.liveFlutterEngine,
          title: title,
          visible: true,
        ),
      );
    }
    else {
      window = await WindowManager.openWindow(
        WindowOptions(
          screenId: screen.id,
          fullscreen: false,
          contentMode: PresentationContentMode.liveFlutterEngine,
          title: title,
          position: WindowPosition(
            x: int.tryParse(_xController.text) ?? 100,
            y: int.tryParse(_yController.text) ?? 100,
          ),
          size: WindowSize(
            width: int.tryParse(_widthController.text) ?? 1280,
            height: int.tryParse(_heightController.text) ?? 720,
          ),
          opacity: _windowOpacity,
          alwaysOnTop: _windowAlwaysOnTop,
          resizable: _windowResizable,
          visible: _windowVisible,
          iconPath: iconPath,
        ),
      );
    }
      

    window.onClosed.listen((_) {
      setState(() {
        _activeWindows.remove(screen.id);
      });
    });

    setState(() {
      _activeWindows[screen.id] = window!;
    });

    // Augmente le délai à 800ms ou 1 seconde pour tester si c'est un problème de timing natif
    await Future.delayed(const Duration(milliseconds: 800));
    window.sendData({'route': _activeRoute.name});
  }

  /// Active/Désactive la fenêtre de retour physique (Floating sur l'écran principal)
  Future<void> _togglePhysicalPreview() async {
    if(Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
      if (_previewWindow != null) {
        await _previewWindow!.close();
        setState(() {
          _previewWindow = null;
        });
        return;
      }

      final primaryScreen = _screens.firstWhere((s) => s.isPrimary, orElse: () => _screens.first);

      final title = _titleController.text.trim().isEmpty
          ? 'Retour Monitoring'
          : _titleController.text.trim();
      final iconPath = _iconPathController.text.trim().isEmpty
          ? null
          : _iconPathController.text.trim();

      final window = await WindowManager.openWindow(
        WindowOptions(
          screenId: primaryScreen.id,
          fullscreen: false,
          contentMode: PresentationContentMode.liveFlutterEngine,
          title: title,
          position: WindowPosition(
            x: int.tryParse(_xController.text) ?? 100,
            y: int.tryParse(_yController.text) ?? 100,
          ),
          size: WindowSize(
            width: int.tryParse(_widthController.text) ?? 1280,
            height: int.tryParse(_heightController.text) ?? 720,
          ),
          opacity: _windowOpacity,
          alwaysOnTop: _windowAlwaysOnTop,
          resizable: _windowResizable,
          visible: _windowVisible,
          iconPath: iconPath,
        ),
      );

      window.onClosed.listen((_) {
        setState(() {
          _previewWindow = null;
        });
      });

      setState(() {
        _previewWindow = window;
      });

      await Future.delayed(const Duration(milliseconds: 300));
      window.sendData({'route': _activeRoute.name});
    }
  }

  Future<void> _applyCurrentWindowSettingsToAll() async {
    final title = _titleController.text.trim().isEmpty
        ? 'Écran de Présentation'
        : _titleController.text.trim();
    final iconPath = _iconPathController.text.trim().isEmpty
        ? null
        : _iconPathController.text.trim();

    for (final window in <Window?>[
      ..._activeWindows.values,
      _previewWindow,
    ].whereType<Window>()) {
      await window.setTitle(title);
      await window.setPosition(
        int.tryParse(_xController.text) ?? 100,
        int.tryParse(_yController.text) ?? 100,
      );
      await window.setSize(
        int.tryParse(_widthController.text) ?? 1280,
        int.tryParse(_heightController.text) ?? 720,
      );
      await window.setOpacity(_windowOpacity);
      await window.setAlwaysOnTop(_windowAlwaysOnTop);
      await window.setResizable(_windowResizable);
      if (_windowVisible) {
        await window.show();
      } else {
        await window.hide();
      }
      await window.setFullScreen(_windowFullscreen);
      await window.setIconPath(iconPath);
    }
  }

  void _changeRoute(AppRoute newRoute) {
    setState(() {
      _activeRoute = newRoute;
    });
    _forceSyncAll();
  }

  void _nextRoute() {
    final nextIndex = (_activeRoute.index + 1) % AppRoute.values.length;
    _changeRoute(AppRoute.values[nextIndex]);
  }

  void _forceSyncAll() {
    for (var window in _activeWindows.values) {
      window.sendData({'route': _activeRoute.name});
    }
    if (_previewWindow != null) {
      _previewWindow!.sendData({'route': _activeRoute.name});
    }
  }

  @override
  Widget build(BuildContext context) {
    final bool anyActive = _activeWindows.isNotEmpty || _previewWindow != null;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Télécommande Multi-Écrans (Stream Intégré)'),
        backgroundColor: Colors.blue.shade100,
      ),
      body: Row(
        children: [
        Material(
          color: Colors.grey.shade100,
          // On utilise un SingleChildScrollView pour permettre le scroll vertical global de la barre
          child: SizedBox(
            width: 360,
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const Text("1. FENÊTRE DE RETOUR PHYSIQUE", style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  ElevatedButton.icon(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: _previewWindow != null ? Colors.purple : Colors.white,
                      foregroundColor: _previewWindow != null ? Colors.white : Colors.purple,
                    ),
                    onPressed: _togglePhysicalPreview,
                    icon: Icon(_previewWindow != null ? Icons.visibility_off : Icons.visibility),
                    label: Text(_previewWindow != null ? "Fermer le retour" : "Activer la fenêtre de retour"),
                  ),
                  const Divider(height: 30),
                  const Text("2. ÉCRANS EXTERNES", style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  ElevatedButton.icon(
                    onPressed: _refreshScreens,
                    icon: const Icon(Icons.refresh),
                    label: const Text("Actualiser la liste"),
                  ),
                  const SizedBox(height: 8),
            
                  // --- MODIFICATION ICI : On enlève le Expanded et on affiche les éléments directement ---
                  ..._screens.map((s) {
                    if (s.isPrimary) return const SizedBox.shrink();
            
                    final isOpened = _activeWindows.containsKey(s.id);
                    final isFullscreen = _screenFullscreenChoices[s.id] ?? false;
            
                    return Card(
                      margin: const EdgeInsets.only(bottom: 10),
                      color: isOpened ? Colors.blue.shade50 : null,
                      child: Padding(
                        padding: const EdgeInsets.all(12.0),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(s.name, style: const TextStyle(fontWeight: FontWeight.bold)),
                            Text("${s.width.toInt()}x${s.height.toInt()}", style: const TextStyle(fontSize: 12, color: Colors.grey)),
                            const SizedBox(height: 8),
                            Row(
                              mainAxisAlignment: MainAxisAlignment.start,
                              children: [
                                Text(isFullscreen ? "Plein Écran Initial" : "Mode Fenêtré (Floating)"),
                                Switch(
                                  value: isFullscreen,
                                  onChanged: isOpened ? null : (val) {
                                    setState(() {
                                      _screenFullscreenChoices[s.id] = val;
                                    });
                                  },
                                ),
                              ],
                            ),
                            const SizedBox(height: 4),
                            SizedBox(
                              width: double.infinity,
                              child: ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: isOpened ? Colors.red : Colors.blue.shade700,
                                  foregroundColor: Colors.white,
                                ),
                                onPressed: () => _toggleScreen(s),
                                child: Text(isOpened ? "Fermer la fenêtre" : "Lancer sur cet écran"),
                              ),
                            ),
                          ],
                        ),
                      ),
                    );
                  }),
                  // -------------------------------------------------------------------------------------
            
                  const Divider(height: 30),
                  const Text("3. OPTIONS DE FENÊTRE", style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _titleController,
                    decoration: const InputDecoration(
                      labelText: 'Titre de la fenêtre',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                  const SizedBox(height: 8),
                  TextField(
                    controller: _iconPathController,
                    decoration: const InputDecoration(
                      labelText: 'Chemin icône optionnel (.ico)',
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: TextField(
                          controller: _xController,
                          keyboardType: TextInputType.number,
                          decoration: const InputDecoration(labelText: 'X', border: OutlineInputBorder(), isDense: true),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: TextField(
                          controller: _yController,
                          keyboardType: TextInputType.number,
                          decoration: const InputDecoration(labelText: 'Y', border: OutlineInputBorder(), isDense: true),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      Expanded(
                        child: TextField(
                          controller: _widthController,
                          keyboardType: TextInputType.number,
                          decoration: const InputDecoration(labelText: 'Largeur', border: OutlineInputBorder(), isDense: true),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: TextField(
                          controller: _heightController,
                          keyboardType: TextInputType.number,
                          decoration: const InputDecoration(labelText: 'Hauteur', border: OutlineInputBorder(), isDense: true),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      const Text('Opacité'),
                      Expanded(
                        child: Slider(
                          value: _windowOpacity,
                          min: 0.1,
                          max: 1.0,
                          divisions: 9,
                          label: _windowOpacity.toStringAsFixed(1),
                          onChanged: (value) => setState(() => _windowOpacity = value),
                        ),
                      ),
                    ],
                  ),
                  SwitchListTile(
                    dense: true,
                    title: const Text('Toujours au premier plan'),
                    value: _windowAlwaysOnTop,
                    onChanged: (value) => setState(() => _windowAlwaysOnTop = value),
                  ),
                  SwitchListTile(
                    dense: true,
                    title: const Text('Redimensionnable'),
                    value: _windowResizable,
                    onChanged: (value) => setState(() => _windowResizable = value),
                  ),
                  SwitchListTile(
                    dense: true,
                    title: const Text('Visible'),
                    value: _windowVisible,
                    onChanged: (value) => setState(() => _windowVisible = value),
                  ),
                  SwitchListTile(
                    dense: true,
                    title: const Text('Plein écran au démarrage'),
                    value: _windowFullscreen,
                    onChanged: (value) => setState(() => _windowFullscreen = value),
                  ),
                  ElevatedButton.icon(
                    onPressed: _applyCurrentWindowSettingsToAll,
                    icon: const Icon(Icons.window),
                    label: const Text('Appliquer aux fenêtres ouvertes'),
                  ),
                  const Divider(height: 30),
                  const Text("4. PILOTAGE DU CONTENU", style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 10),
                  ElevatedButton.icon(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: anyActive ? Colors.green : Colors.grey,
                      foregroundColor: Colors.white,
                      padding: const EdgeInsets.symmetric(vertical: 12),
                    ),
                    onPressed: anyActive ? _nextRoute : null,
                    icon: const Icon(Icons.skip_next),
                    label: const Text("SLIDE SUIVANTE", style: TextStyle(fontWeight: FontWeight.bold)),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: AppRoute.values.map((route) {
                      final isSelected = _activeRoute == route;
                      return Expanded(
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 2.0),
                          child: ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              backgroundColor: isSelected ? Colors.blue : Colors.white,
                              foregroundColor: isSelected ? Colors.white : Colors.black87,
                              padding: const EdgeInsets.symmetric(vertical: 4),
                            ),
                            onPressed: () => _changeRoute(route),
                            child: Text(route.name.substring(0, 3).toUpperCase(), style: const TextStyle(fontSize: 11)),
                          ),
                        ),
                      );
                    }).toList(),
                  ),
                ],
              ),
            ),
          ),
        ),
        ],
      ),
    );
  }
}
3
likes
160
points
552
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A comprehensive Flutter plugin to manage presentation windows on secondary screens with customizable properties.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on multi_screen_presentation

Packages that implement multi_screen_presentation