document_pip 0.1.1 copy "document_pip: ^0.1.1" to clipboard
document_pip: ^0.1.1 copied to clipboard

Platformweb

Put live Flutter widgets in a real always-on-top OS window from Flutter Web, using the Document Picture-in-Picture API and Flutter's multi-view engine.

example/lib/main.dart

// A player whose mini view pops out into a real floating window.
//
// The point of the demo is the shared state: `_Playback` sits ABOVE
// DocumentPipApp, so the page and the floating window are looking at the same
// object. Scrub in one and the other moves, because there is only one.
import 'package:document_pip/document_pip.dart';
import 'package:flutter/material.dart';

void main() => runWidget(buildApp());

/// The whole app, exposed so the smoke test can mount the same tree.
Widget buildApp() => _Playback(
  child: DocumentPipApp(
    main: (BuildContext context) => const _Shell(child: PageView2()),
    popOut: (BuildContext context) => const _Shell(child: MiniPlayer()),
  ),
);

/// One clock, shared by every window.
class _Playback extends StatefulWidget {
  const _Playback({required this.child});
  final Widget child;

  static _PlaybackState of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<_Scope>()!.state;

  @override
  State<_Playback> createState() => _PlaybackState();
}

class _PlaybackState extends State<_Playback>
    with SingleTickerProviderStateMixin {
  late final AnimationController _c = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 30),
  )..addListener(() => setState(() {}));

  bool get playing => _c.isAnimating;
  double get progress => _c.value;

  void toggle() => playing ? _c.stop() : _c.repeat();
  void seek(double v) => _c.value = v;

  @override
  void initState() {
    super.initState();
    _c.repeat();
  }

  @override
  void dispose() {
    _c.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) =>
      _Scope(state: this, child: widget.child);
}

class _Scope extends InheritedWidget {
  const _Scope({required this.state, required super.child});
  final _PlaybackState state;

  @override
  bool updateShouldNotify(_Scope old) => true;
}

class _Shell extends StatelessWidget {
  const _Shell({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      colorSchemeSeed: const Color(0xFF3DDC97),
      brightness: Brightness.dark,
      useMaterial3: true,
    ),
    home: child,
  );
}

/// The page.
class PageView2 extends StatefulWidget {
  const PageView2({super.key});
  @override
  State<PageView2> createState() => _PageView2State();
}

class _PageView2State extends State<PageView2> {
  String _status = '';

  Future<void> _popOut() async {
    // First await in the handler, deliberately: anything before this spends
    // the user gesture and the browser refuses.
    try {
      final PipWindow w = await DocumentPip.open(width: 380, height: 210);
      setState(() => _status = 'floating in its own window');
      await w.closed;
      if (mounted) setState(() => _status = 'window closed');
    } on DocumentPipException catch (e) {
      if (mounted) setState(() => _status = e.message.split('\n').first);
    }
  }

  @override
  Widget build(BuildContext context) {
    final _PlaybackState p = _Playback.of(context);
    return Scaffold(
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 460),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Text(
                'document_pip',
                style: Theme.of(context).textTheme.headlineMedium,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 6),
              Text(
                'Pop the player out. It keeps running, above everything else.',
                style: Theme.of(context).textTheme.bodyMedium,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 28),
              const _Scrubber(),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  IconButton.filledTonal(
                    onPressed: p.toggle,
                    iconSize: 30,
                    icon: Icon(p.playing ? Icons.pause : Icons.play_arrow),
                  ),
                  const SizedBox(width: 12),
                  FilledButton.icon(
                    onPressed: DocumentPip.isSupported ? _popOut : null,
                    icon: const Icon(Icons.picture_in_picture_alt),
                    label: const Text('Pop out'),
                  ),
                ],
              ),
              const SizedBox(height: 14),
              Text(
                DocumentPip.isSupported
                    ? _status
                    : 'This browser has no Document Picture-in-Picture — '
                          'Chrome, Edge or Firefox 151+ is needed.',
                style: Theme.of(context).textTheme.bodySmall,
                textAlign: TextAlign.center,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// What the floating window shows.
class MiniPlayer extends StatelessWidget {
  const MiniPlayer({super.key});

  @override
  Widget build(BuildContext context) {
    final _PlaybackState p = _Playback.of(context);
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(18),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Text('Now playing', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            Text(
              'same widget tree, different window',
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: 18),
            const _Scrubber(),
            const SizedBox(height: 10),
            Align(
              child: IconButton.filledTonal(
                onPressed: p.toggle,
                icon: Icon(p.playing ? Icons.pause : Icons.play_arrow),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _Scrubber extends StatelessWidget {
  const _Scrubber();

  @override
  Widget build(BuildContext context) {
    final _PlaybackState p = _Playback.of(context);
    return Slider(value: p.progress, onChanged: p.seek);
  }
}
0
likes
160
points
0
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publisherjumyn.com

Weekly Downloads

Put live Flutter widgets in a real always-on-top OS window from Flutter Web, using the Document Picture-in-Picture API and Flutter's multi-view engine.

Repository (GitHub)
View/report issues

Topics

#picture-in-picture #web #window #multi-view #floating

License

MIT (license)

Dependencies

flutter, web

More

Packages that depend on document_pip