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>
This commit is contained in:
2026-08-09 21:14:54 +02:00
parent 55c396b2a2
commit 2f5edfc512
74 changed files with 630 additions and 72 deletions
+77 -39
View File
@@ -1,6 +1,9 @@
import "dart:async";
import "dart:io";
import "package:dartamp_skin/dartamp_skin.dart";
import "package:desktop_multi_window/desktop_multi_window.dart";
import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:window_manager/window_manager.dart";
@@ -8,8 +11,7 @@ import "package:window_manager/window_manager.dart";
import "src/common_widgets.dart";
import "src/main_window/main_window.dart";
import "src/player/player_state.dart";
import "src/skin/classic_skin.dart";
import "src/skin/skin_loader.dart";
import "src/skin/skin_manager.dart";
import "src/windows/sub_window_app.dart";
import "src/windows/sub_window_channel.dart";
import "src/windows/window_args.dart";
@@ -35,6 +37,7 @@ Future<void> main(List<String> args) async {
type: subWindowArgs.type,
initialShade: subWindowArgs.shade,
initialAlwaysOnTop: subWindowArgs.alwaysOnTop,
initialSkinName: subWindowArgs.skinName,
),
),
);
@@ -46,6 +49,7 @@ Future<void> main(List<String> args) async {
final eqOpen = await PlayerState.loadEqOpen();
final plOpen = await PlayerState.loadPlOpen();
final uiScale = await PlayerState.loadUiScale();
final skinManager = await SkinManager.load();
await windowManager.ensureInitialized();
runApp(
DartampApp(
@@ -54,6 +58,7 @@ Future<void> main(List<String> args) async {
initialEqOpen: eqOpen,
initialPlOpen: plOpen,
initialUiScale: uiScale,
skinManager: skinManager,
),
);
}
@@ -75,6 +80,7 @@ class DartampApp extends StatelessWidget {
required this.initialEqOpen,
required this.initialPlOpen,
required this.initialUiScale,
required this.skinManager,
super.key,
});
@@ -83,6 +89,7 @@ class DartampApp extends StatelessWidget {
final bool initialEqOpen;
final bool initialPlOpen;
final double initialUiScale;
final SkinManager skinManager;
@override
Widget build(BuildContext context) {
@@ -94,6 +101,7 @@ class DartampApp extends StatelessWidget {
initialEqOpen: initialEqOpen,
initialPlOpen: initialPlOpen,
initialUiScale: initialUiScale,
skinManager: skinManager,
),
);
}
@@ -106,6 +114,7 @@ class WinampRoot extends StatefulWidget {
required this.initialEqOpen,
required this.initialPlOpen,
required this.initialUiScale,
required this.skinManager,
super.key,
});
@@ -114,6 +123,7 @@ class WinampRoot extends StatefulWidget {
final bool initialEqOpen;
final bool initialPlOpen;
final double initialUiScale;
final SkinManager skinManager;
@override
State<WinampRoot> createState() => _WinampRootState();
@@ -121,12 +131,14 @@ class WinampRoot extends StatefulWidget {
class _WinampRootState extends State<WinampRoot> {
late final PlayerState _player;
late final SkinManager _skins;
bool _windowReady = false;
bool _wasShade = false;
double _wasScale = 1.0;
bool _wasAlwaysOnTop = false;
bool _wasEqOpen = false;
bool _wasPlOpen = false;
String _wasSkinName = baseSkinName;
WindowController? _eqController;
WindowController? _plController;
StreamSubscription<void>? _windowsSubscription;
@@ -141,12 +153,15 @@ class _WinampRootState extends State<WinampRoot> {
plOpen: widget.initialPlOpen,
uiScale: widget.initialUiScale,
);
_skins = widget.skinManager;
_wasShade = widget.initialWindowShade;
_wasScale = widget.initialUiScale;
_wasAlwaysOnTop = widget.initialAlwaysOnTop;
_wasEqOpen = widget.initialEqOpen;
_wasPlOpen = widget.initialPlOpen;
_wasSkinName = _skins.active.name;
_player.addListener(_onPlayerChanged);
_skins.addListener(_onSkinChanged);
// Set up method channel for macOS menu actions
_winampChannel.setMethodCallHandler(_handleMethodCall);
@@ -171,6 +186,7 @@ class _WinampRootState extends State<WinampRoot> {
void dispose() {
_windowsSubscription?.cancel();
_player.removeListener(_onPlayerChanged);
_skins.removeListener(_onSkinChanged);
_player.dispose();
super.dispose();
}
@@ -249,6 +265,30 @@ class _WinampRootState extends State<WinampRoot> {
_player.setPlOpen(!_player.plOpen);
}
void _onSkinChanged() {
final name = _skins.active.name;
if (name == _wasSkinName) return;
_wasSkinName = name;
_eqController?.invokeMethod("setSkin", name);
_plController?.invokeMethod("setSkin", name);
}
Future<void> _importSkin() async {
final result = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: const ["wsz", "zip"],
dialogTitle: "Load Winamp Skin",
);
final path = result?.files.single.path;
if (path == null) return;
try {
final bytes = await File(path).readAsBytes();
await _skins.importArchive(bytes, fileName: path.split("/").last);
} catch (_) {
// ignore: a failed import leaves the active skin unchanged
}
}
Future<void> _openSubWindow(SubWindowType type) async {
final isEq = type == SubWindowType.eq;
var controller = isEq ? _eqController : _plController;
@@ -264,6 +304,7 @@ class _WinampRootState extends State<WinampRoot> {
shade: isEq ? _player.eqShade : _player.plShade,
alwaysOnTop: _player.alwaysOnTop,
uiScale: _player.uiScale,
skinName: _skins.active.name,
).encode(),
hiddenAtLaunch: true,
),
@@ -329,6 +370,8 @@ class _WinampRootState extends State<WinampRoot> {
case "setUIScale":
final arguments = call.arguments as Map;
_player.setUiScale((arguments["scale"] as num).toDouble());
case "importSkin":
await _importSkin();
}
}
@@ -356,45 +399,40 @@ class _WinampRootState extends State<WinampRoot> {
@override
Widget build(BuildContext context) {
return FutureBuilder<ClassicSkin>(
future: loadBaseSkin(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return ColoredBox(
color: const Color(0xFF00FF00),
child: Center(child: Text("Skin load failed: ${snapshot.error}")),
);
}
if (!snapshot.hasData) {
return const ColoredBox(color: Colors.black);
}
return AnimatedBuilder(
animation: _player,
builder: (context, _) {
final scale = _player.uiScale;
final height =
(_player.windowShade ? _shadeSize : _mainSize).height;
return ColoredBox(
color: const Color(0xFF000000),
child: DragSurface(
draggable: _windowReady,
onWindowShadeToggle: _player.toggleWindowShade,
child: SizedBox(
width: _mainSize.width * scale,
height: height * scale,
child: Transform.scale(
scale: scale,
alignment: Alignment.topLeft,
filterQuality: FilterQuality.none,
child: MainWindowView(
skin: snapshot.data!,
player: _player,
),
),
final skin = _skins.skin;
if (skin == null) {
return const ColoredBox(color: Colors.black);
}
if (_skins.lastError != null) {
return ColoredBox(
color: const Color(0xFF00FF00),
child: Center(child: Text("Skin load failed: ${_skins.lastError}")),
);
}
return AnimatedBuilder(
animation: Listenable.merge([_player, _skins]),
builder: (context, _) {
final scale = _player.uiScale;
final height = (_player.windowShade ? _shadeSize : _mainSize).height;
return ColoredBox(
color: const Color(0xFF000000),
child: DragSurface(
draggable: _windowReady,
onWindowShadeToggle: _player.toggleWindowShade,
child: SizedBox(
width: _mainSize.width * scale,
height: height * scale,
child: Transform.scale(
scale: scale,
alignment: Alignment.topLeft,
filterQuality: FilterQuality.none,
child: MainWindowView(
skin: skin,
player: _player,
),
),
);
},
),
),
);
},
);