import "dart:async"; import "package:dartamp_skin/dartamp_skin.dart"; import "package:desktop_multi_window/desktop_multi_window.dart"; import "package:flutter/material.dart"; import "package:window_manager/window_manager.dart"; import "../main_window/eq_window.dart"; import "../main_window/playlist_window.dart"; import "../player/player_state.dart"; import "../skin/skin_manager.dart"; import "sub_window_channel.dart"; import "window_args.dart"; const Size _eqMainSize = Size(275, 116); const Size _eqShadeSize = Size(275, 14); const Size _plMainSize = Size(280, 290); const Size _plShadeSize = Size(280, 14); /// The root of an Equalizer or Playlist sub-window. /// /// Each sub-window is an independent Flutter engine that re-runs the app's /// `main`. This root configures the native window (size, frameless look, /// always-on-top), shows it once configured, reacts to always-on-top pushes /// from the main window, and forwards shade toggles and close requests back to /// the main window over [subWindowChannel]. class SubWindowRoot extends StatefulWidget { const SubWindowRoot({ required this.type, required this.initialShade, required this.initialAlwaysOnTop, this.initialUiScale = 1.0, this.initialSkinName = "Base", super.key, }); final SubWindowType type; final bool initialShade; final bool initialAlwaysOnTop; final double initialUiScale; final String initialSkinName; @override State createState() => _SubWindowRootState(); } class _SubWindowRootState extends State { late final PlayerState _player; late Future _skinFuture; bool _windowReady = false; bool _wasShade = false; double _wasScale = 1.0; bool get _isEq => widget.type == SubWindowType.eq; Size get _mainSize => _isEq ? _eqMainSize : _plMainSize; Size get _shadeSize => _isEq ? _eqShadeSize : _plShadeSize; @override void initState() { super.initState(); _player = PlayerState( alwaysOnTop: widget.initialAlwaysOnTop, eqShade: _isEq ? widget.initialShade : false, plShade: _isEq ? false : widget.initialShade, uiScale: widget.initialUiScale, ); _skinFuture = loadAppSkin(widget.initialSkinName); _wasShade = widget.initialShade; _wasScale = widget.initialUiScale; _player.addListener(_onPlayerChanged); WidgetsBinding.instance.addPostFrameCallback((_) { _readyWindow(); }); unawaited(_wireWindowControl()); } @override void dispose() { _player.removeListener(_onPlayerChanged); _player.dispose(); super.dispose(); } /// Handles pushes from the main window on this window's own control channel. Future _wireWindowControl() async { try { final controller = await WindowController.fromCurrentEngine(); await controller.setWindowMethodHandler((call) async { switch (call.method) { case "setAlwaysOnTop": if (call.arguments is bool) { _player.setAlwaysOnTop(call.arguments as bool); } case "setUiScale": if (call.arguments is num) { _player.setUiScale((call.arguments as num).toDouble()); } case "setSkin": if (call.arguments is String) { setState(() { _skinFuture = loadAppSkin(call.arguments as String); }); } } }); } catch (_) { // ignore: control channel unavailable until the window is registered } } Future _readyWindow() async { if (!mounted || _windowReady) return; try { await windowManager.setPreventClose(false); await windowManager.setResizable(false); await windowManager.setAlwaysOnTop(_player.alwaysOnTop); await windowManager.setTitle( _isEq ? "Winamp Equalizer" : "Winamp Playlist", ); await windowManager.setAsFrameless(); await windowManager.setBackgroundColor(const Color(0xFF000000)); await windowManager.setSize(_scaledSize()); await windowManager.center(); _windowReady = true; final controller = await WindowController.fromCurrentEngine(); await controller.show(); } catch (_) { // The native window may not be ready yet; defer to the next frame so the // window still shows once it is. WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _readyWindow(); }); } } Size _scaledSize() { final base = _wasShade ? _shadeSize : _mainSize; return Size(base.width * _player.uiScale, base.height * _player.uiScale); } void _onPlayerChanged() { if (!_windowReady) return; final shade = _isEq ? _player.eqShade : _player.plShade; final shadeChanged = shade != _wasShade; final scaleChanged = _player.uiScale != _wasScale; if (shadeChanged) { _wasShade = shade; unawaited( subWindowChannel.invokeMethod( _isEq ? "toggleEqShade" : "togglePlShade", ), ); } if (shadeChanged || scaleChanged) { _wasScale = _player.uiScale; windowManager.setSize(_scaledSize()); } windowManager.setAlwaysOnTop(_player.alwaysOnTop); } @override Widget build(BuildContext context) { return ColoredBox( color: const Color(0xFF000000), child: AnimatedBuilder( animation: _player, builder: (context, _) { final scale = _player.uiScale; final base = (_isEq ? _player.eqShade : _player.plShade) ? _shadeSize : _mainSize; return SizedBox( width: base.width * scale, height: base.height * scale, child: Transform.scale( scale: scale, alignment: Alignment.topLeft, filterQuality: FilterQuality.none, child: _isEq ? EqWindow(player: _player, skinFuture: _skinFuture) : PlWindow(player: _player, skinFuture: _skinFuture), ), ); }, ), ); } }