Files
dartamp/lib/src/windows/sub_window_app.dart
T
hans 2f5edfc512 feat: update dependencies and integrate skin manager
- Added `dartamp_skin` as a local dependency in `pubspec.yaml`.
- Updated `path_provider` to version ^2.1.5.
- Modified `test_driver/app.dart` to initialize `SkinManager` and pass it to `DartampApp`.
- Updated `generated_plugins.cmake` to include `jni` in the FFI plugin list.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
2026-08-09 21:14:54 +02:00

190 lines
5.9 KiB
Dart

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<SubWindowRoot> createState() => _SubWindowRootState();
}
class _SubWindowRootState extends State<SubWindowRoot> {
late final PlayerState _player;
late Future<ClassicSkin> _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<void> _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<void> _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),
),
);
},
),
);
}
}