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>
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:dartamp/src/skin/skin_image.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:window_manager/window_manager.dart";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import "dart:async";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "../common_widgets.dart";
|
||||
import "../player/player_state.dart";
|
||||
import "../skin/classic_skin.dart";
|
||||
import "../skin/skin_image.dart";
|
||||
import "../skin/skin_loader.dart";
|
||||
import "../windows/sub_window_channel.dart";
|
||||
|
||||
const Size _eqMainSize = Size(275, 116);
|
||||
@@ -20,14 +18,15 @@ const Size _eqShadeSize = Size(275, 14);
|
||||
/// presets buttons, the response graph, and 11 draggable sliders (preamp + 10
|
||||
/// bands) drawn on top. Windowshade mode uses `eq_ex.bmp`.
|
||||
class EqWindow extends StatelessWidget {
|
||||
const EqWindow({required this.player, super.key});
|
||||
const EqWindow({required this.player, this.skinFuture, super.key});
|
||||
|
||||
final PlayerState player;
|
||||
final Future<ClassicSkin>? skinFuture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<ClassicSkin>(
|
||||
future: loadBaseSkin(),
|
||||
future: skinFuture ?? loadBaseSkin(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return ColoredBox(
|
||||
|
||||
@@ -2,15 +2,12 @@ import "dart:async";
|
||||
import "dart:math";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:window_manager/window_manager.dart";
|
||||
|
||||
import "../common_widgets.dart";
|
||||
import "../player/player_state.dart";
|
||||
import "../skin/classic_skin.dart";
|
||||
import "../skin/skin_image.dart";
|
||||
import "../skin/skin_slider.dart";
|
||||
import "../skin/text_font.dart";
|
||||
|
||||
const Size _mainSize = Size(275, 116);
|
||||
const Size _wsSize = Size(275, 14);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import "dart:async";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "../common_widgets.dart";
|
||||
import "../player/player_state.dart";
|
||||
import "../skin/classic_skin.dart";
|
||||
import "../skin/skin_image.dart";
|
||||
import "../skin/skin_loader.dart";
|
||||
import "../skin/text_font.dart";
|
||||
import "../windows/sub_window_channel.dart";
|
||||
|
||||
const Size _plMainSize = Size(280, 290);
|
||||
@@ -152,14 +149,15 @@ class _KeyedFont {
|
||||
/// Lives in its own OS window (its own Flutter engine); the parent
|
||||
/// [SubWindowRoot] manages native window sizing and cross-window messaging.
|
||||
class PlWindow extends StatelessWidget {
|
||||
const PlWindow({required this.player, super.key});
|
||||
const PlWindow({required this.player, this.skinFuture, super.key});
|
||||
|
||||
final PlayerState player;
|
||||
final Future<ClassicSkin>? skinFuture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<ClassicSkin>(
|
||||
future: loadBaseSkin(),
|
||||
future: skinFuture ?? loadBaseSkin(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return ColoredBox(
|
||||
|
||||
@@ -10,6 +10,7 @@ const String _kEqShadeKey = "winamp.eq_shade";
|
||||
const String _kPlOpenKey = "winamp.pl_open";
|
||||
const String _kPlShadeKey = "winamp.pl_shade";
|
||||
const String _kUiScaleKey = "winamp.ui_scale";
|
||||
const String _kActiveSkinKey = "winamp.active_skin";
|
||||
|
||||
/// Simulated playback/UI state for the player, mirroring the flags tracked by
|
||||
/// the original Winamp main window. No audio yet; the transport simply drives
|
||||
@@ -291,6 +292,22 @@ class PlayerState extends ChangeNotifier {
|
||||
return (prefs.getDouble(_kUiScaleKey) ?? 1.0).clamp(1.0, 3.0);
|
||||
}
|
||||
|
||||
/// The name of the active skin, or `null` when the default (Base) applies.
|
||||
static Future<String?> loadActiveSkin() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_kActiveSkinKey);
|
||||
}
|
||||
|
||||
/// Persists the active skin name; `null` restores the default.
|
||||
static Future<void> saveActiveSkin(String? name) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (name == null) {
|
||||
await prefs.remove(_kActiveSkinKey);
|
||||
} else {
|
||||
await prefs.setString(_kActiveSkinKey, name);
|
||||
}
|
||||
}
|
||||
|
||||
void _startTicker() {
|
||||
_ticker?.cancel();
|
||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
|
||||
import "../player/player_state.dart";
|
||||
|
||||
/// Resolves the directory holding the extracted skins, creating it on first
|
||||
/// use. Lives under the app-support folder so imported skins never touch the
|
||||
/// original Winamp files.
|
||||
Future<Directory> skinsDirectory() async {
|
||||
final support = await getApplicationSupportDirectory();
|
||||
return Directory("${support.path}/dartamp/skins");
|
||||
}
|
||||
|
||||
/// Loads the named skin for an engine that has no [SkinManager] of its own
|
||||
/// (the Equalizer/Playlist sub-windows). Falls back to the bundled base skin
|
||||
/// when the name is unknown or the load fails.
|
||||
Future<ClassicSkin> loadAppSkin(String name) async {
|
||||
try {
|
||||
final registry = await SkinRegistry.load(await skinsDirectory());
|
||||
final info = registry.byName(name);
|
||||
if (info == null) return loadBaseSkin();
|
||||
return await registry.loadSkin(info);
|
||||
} catch (_) {
|
||||
return loadBaseSkin();
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the installed skins, the active selection, and the loaded
|
||||
/// [ClassicSkin] for the main window.
|
||||
///
|
||||
/// The active skin is persisted across launches and broadcast to the
|
||||
/// Equalizer/Playlist sub-windows by whoever owns the manager.
|
||||
class SkinManager extends ChangeNotifier {
|
||||
SkinManager._(this._registry, this._active);
|
||||
|
||||
final SkinRegistry _registry;
|
||||
SkinInfo _active;
|
||||
ClassicSkin? _skin;
|
||||
String? _lastError;
|
||||
|
||||
/// The installed skins, base first, in scan order.
|
||||
List<SkinInfo> get skins => _registry.skins;
|
||||
|
||||
/// The currently selected skin.
|
||||
SkinInfo get active => _active;
|
||||
|
||||
/// The loaded skin, or `null` until [load] completes.
|
||||
ClassicSkin? get skin => _skin;
|
||||
|
||||
/// The last load error, or `null` when the last load succeeded.
|
||||
String? get lastError => _lastError;
|
||||
|
||||
/// Scans the skins directory and activates the saved skin (defaults to the
|
||||
/// bundled base skin).
|
||||
static Future<SkinManager> load() async {
|
||||
final registry = await SkinRegistry.load(await skinsDirectory());
|
||||
final saved = await PlayerState.loadActiveSkin();
|
||||
final savedInfo = saved == null ? null : registry.byName(saved);
|
||||
final active = savedInfo ?? registry.skins.first;
|
||||
final manager = SkinManager._(registry, active);
|
||||
await manager._reload();
|
||||
return manager;
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
_lastError = null;
|
||||
try {
|
||||
_skin = await _registry.loadSkin(_active);
|
||||
} catch (error) {
|
||||
_lastError = "$error";
|
||||
_active = _registry.skins.first;
|
||||
_skin = await loadBaseSkin();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Selects [info] as the active skin, persists it, and reloads.
|
||||
Future<void> apply(SkinInfo info) async {
|
||||
if (info.name == _active.name && _skin != null) return;
|
||||
_active = info;
|
||||
await PlayerState.saveActiveSkin(info.name);
|
||||
await _reload();
|
||||
}
|
||||
|
||||
/// Imports a `.wsz` archive and applies it immediately.
|
||||
Future<SkinInfo> importArchive(
|
||||
Uint8List bytes, {
|
||||
String? name,
|
||||
String? fileName,
|
||||
}) async {
|
||||
final info = await _registry.importArchive(
|
||||
bytes,
|
||||
name: name,
|
||||
fileName: fileName,
|
||||
);
|
||||
await apply(info);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -7,6 +8,7 @@ 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";
|
||||
|
||||
@@ -28,6 +30,7 @@ class SubWindowRoot extends StatefulWidget {
|
||||
required this.initialShade,
|
||||
required this.initialAlwaysOnTop,
|
||||
this.initialUiScale = 1.0,
|
||||
this.initialSkinName = "Base",
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -35,6 +38,7 @@ class SubWindowRoot extends StatefulWidget {
|
||||
final bool initialShade;
|
||||
final bool initialAlwaysOnTop;
|
||||
final double initialUiScale;
|
||||
final String initialSkinName;
|
||||
|
||||
@override
|
||||
State<SubWindowRoot> createState() => _SubWindowRootState();
|
||||
@@ -42,6 +46,7 @@ class SubWindowRoot extends StatefulWidget {
|
||||
|
||||
class _SubWindowRootState extends State<SubWindowRoot> {
|
||||
late final PlayerState _player;
|
||||
late Future<ClassicSkin> _skinFuture;
|
||||
bool _windowReady = false;
|
||||
bool _wasShade = false;
|
||||
double _wasScale = 1.0;
|
||||
@@ -60,6 +65,7 @@ class _SubWindowRootState extends State<SubWindowRoot> {
|
||||
plShade: _isEq ? false : widget.initialShade,
|
||||
uiScale: widget.initialUiScale,
|
||||
);
|
||||
_skinFuture = loadAppSkin(widget.initialSkinName);
|
||||
_wasShade = widget.initialShade;
|
||||
_wasScale = widget.initialUiScale;
|
||||
_player.addListener(_onPlayerChanged);
|
||||
@@ -90,6 +96,12 @@ class _SubWindowRootState extends State<SubWindowRoot> {
|
||||
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 (_) {
|
||||
@@ -166,8 +178,8 @@ class _SubWindowRootState extends State<SubWindowRoot> {
|
||||
alignment: Alignment.topLeft,
|
||||
filterQuality: FilterQuality.none,
|
||||
child: _isEq
|
||||
? EqWindow(player: _player)
|
||||
: PlWindow(player: _player),
|
||||
? EqWindow(player: _player, skinFuture: _skinFuture)
|
||||
: PlWindow(player: _player, skinFuture: _skinFuture),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ class SubWindowArgs {
|
||||
required this.shade,
|
||||
required this.alwaysOnTop,
|
||||
this.uiScale = 1.0,
|
||||
this.skinName = "Base",
|
||||
});
|
||||
|
||||
final SubWindowType type;
|
||||
@@ -24,6 +25,9 @@ class SubWindowArgs {
|
||||
/// same size/zoom. Updated live over the control channel afterwards.
|
||||
final double uiScale;
|
||||
|
||||
/// The active skin at spawn time, so the sub-window starts skinned.
|
||||
final String skinName;
|
||||
|
||||
/// Encodes the args into the JSON string the plugin passes to the new
|
||||
/// window's engine.
|
||||
String encode() {
|
||||
@@ -32,6 +36,7 @@ class SubWindowArgs {
|
||||
"shade": shade,
|
||||
"alwaysOnTop": alwaysOnTop,
|
||||
"uiScale": uiScale,
|
||||
"skinName": skinName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +61,7 @@ class SubWindowArgs {
|
||||
shade: map["shade"] == true,
|
||||
alwaysOnTop: map["alwaysOnTop"] == true,
|
||||
uiScale: (map["uiScale"] as num?)?.toDouble().clamp(1.0, 3.0) ?? 1.0,
|
||||
skinName: (map["skinName"] as String?) ?? "Base",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -161,4 +161,8 @@ class AppDelegate: FlutterAppDelegate {
|
||||
@IBAction func setUIScale3x(_ sender: NSMenuItem) {
|
||||
send("setUIScale", ["scale": 3.0])
|
||||
}
|
||||
|
||||
@IBAction func importSkin(_ sender: NSMenuItem) {
|
||||
send("importSkin")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem title="Load Skin…" id="mnu-load-skin">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="importSkin:" target="Voe-Tx-rLC" id="act-import-skin"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include: package:arcane_analysis/analysis_options.yaml
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,16 @@
|
||||
/// Classic Winamp skin support for dartamp.
|
||||
///
|
||||
/// Provides the [ClassicSkin] data model, BMP decoding, `.wsz`/base-skin
|
||||
/// loading, and the small skin-rendering widgets ([SkinImage], [SkinSlider],
|
||||
/// [SkinText]) that blit crops of skin bitmaps the way the original GDI code
|
||||
/// did.
|
||||
library;
|
||||
|
||||
export "src/bmp_decoder.dart";
|
||||
export "src/classic_skin.dart";
|
||||
export "src/skin_image.dart";
|
||||
export "src/skin_info.dart";
|
||||
export "src/skin_loader.dart";
|
||||
export "src/skin_registry.dart";
|
||||
export "src/skin_slider.dart";
|
||||
export "src/text_font.dart";
|
||||
@@ -0,0 +1,18 @@
|
||||
import "dart:io";
|
||||
|
||||
/// Describes an installed skin.
|
||||
///
|
||||
/// The bundled base skin has a null [directory]; imported skins point at the
|
||||
/// directory their extracted `.wsz` files live in.
|
||||
class SkinInfo {
|
||||
const SkinInfo({required this.name, this.directory});
|
||||
|
||||
/// Display name shown in skin lists.
|
||||
final String name;
|
||||
|
||||
/// Directory containing the skin's files, or null for the bundled base.
|
||||
final Directory? directory;
|
||||
|
||||
/// Whether this is the built-in base skin rather than a user import.
|
||||
bool get isBundled => directory == null;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
@@ -65,11 +66,13 @@ Future<ui.Image> bmpToUiImage(BmpImage bmp) {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Loads the bundled base skin from Flutter assets.
|
||||
/// Loads the bundled base skin from the package's Flutter assets.
|
||||
Future<ClassicSkin> loadBaseSkin() async {
|
||||
final images = <String, ui.Image>{};
|
||||
for (final name in bundledSkinFiles) {
|
||||
final data = await rootBundle.load("assets/skins/base/$name");
|
||||
final data = await rootBundle.load(
|
||||
"packages/dartamp_skin/assets/skins/base/$name",
|
||||
);
|
||||
final bytes = data.buffer.asUint8List(
|
||||
data.offsetInBytes,
|
||||
data.lengthInBytes,
|
||||
@@ -95,7 +98,25 @@ Future<ClassicSkin> loadSkinFromArchive(
|
||||
if (key.contains("\\") || key.contains("/")) continue;
|
||||
files[key] = file.content as List<int>;
|
||||
}
|
||||
return _buildSkin(base, files, name: name ?? _skinDisplayName(archive));
|
||||
return _buildSkin(base, files, name: name ?? skinDisplayName(archive));
|
||||
}
|
||||
|
||||
/// Loads a previously-extracted skin directory (see [SkinRegistry.import])
|
||||
/// on top of [base]. Only top-level files are read; anything in a
|
||||
/// subdirectory is ignored, mirroring the flat archive layout.
|
||||
Future<ClassicSkin> loadSkinFromDirectory(
|
||||
ClassicSkin base,
|
||||
Directory directory, {
|
||||
String? name,
|
||||
}) async {
|
||||
final files = <String, List<int>>{};
|
||||
for (final entity in directory.listSync()) {
|
||||
if (entity is! File) continue;
|
||||
final key = entity.uri.pathSegments.last.toLowerCase();
|
||||
if (key.contains("\\") || key.contains("/")) continue;
|
||||
files[key] = await entity.readAsBytes();
|
||||
}
|
||||
return _buildSkin(base, files, name: name);
|
||||
}
|
||||
|
||||
Future<ClassicSkin> _buildSkin(
|
||||
@@ -144,14 +165,16 @@ Future<ClassicSkin> _buildSkin(
|
||||
);
|
||||
}
|
||||
|
||||
String _skinDisplayName(Archive archive) {
|
||||
/// Best-effort display name for an archive: the `id` attribute from its
|
||||
/// `skin.xml`, or null when the archive has none.
|
||||
String? skinDisplayName(Archive archive) {
|
||||
final xml = archive.files.where((f) => f.name.toLowerCase() == "skin.xml");
|
||||
if (xml.isNotEmpty) {
|
||||
final content = utf8.decode(xml.first.content as List<int>);
|
||||
final m = RegExp(r'id\s*=\s*"([^"]+)"').firstMatch(content);
|
||||
if (m != null) return m.group(1)!;
|
||||
}
|
||||
return "Skin";
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,148 @@
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:archive/archive.dart";
|
||||
|
||||
import "classic_skin.dart";
|
||||
import "skin_info.dart";
|
||||
import "skin_loader.dart";
|
||||
|
||||
/// The name of the built-in base skin in [skins].
|
||||
const String baseSkinName = "Base";
|
||||
|
||||
/// Indexes the installed skins in a directory and loads them on top of the
|
||||
/// bundled base skin.
|
||||
///
|
||||
/// The registry scans [Directory] subfolders for extracted `.wsz` skins; each
|
||||
/// folder may carry a `manifest.json` written by [SkinRegistry.importArchive]
|
||||
/// with the skin's display name. The bundled base skin is always listed.
|
||||
class SkinRegistry {
|
||||
SkinRegistry._(this._directory, List<SkinInfo> skins)
|
||||
: _skins = List<SkinInfo>.of(skins);
|
||||
|
||||
/// Directory the imported skins live in.
|
||||
final Directory _directory;
|
||||
|
||||
final List<SkinInfo> _skins;
|
||||
|
||||
static const String manifestName = "manifest.json";
|
||||
|
||||
/// Scans [directory] for imported skins. The directory is created if it
|
||||
/// does not exist yet.
|
||||
static Future<SkinRegistry> load(Directory directory) async {
|
||||
if (!await directory.exists()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
return SkinRegistry._(directory, _scan(directory));
|
||||
}
|
||||
|
||||
static List<SkinInfo> _scan(Directory directory) {
|
||||
final result = <SkinInfo>[
|
||||
const SkinInfo(name: baseSkinName),
|
||||
];
|
||||
for (final entity in directory.listSync()) {
|
||||
if (entity is! Directory) continue;
|
||||
final name = _displayNameFor(entity);
|
||||
result.add(SkinInfo(name: name, directory: entity));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _displayNameFor(Directory directory) {
|
||||
final manifest = File(
|
||||
directory.uri.resolve(manifestName).toFilePath(),
|
||||
);
|
||||
if (manifest.existsSync()) {
|
||||
try {
|
||||
final map = jsonDecode(manifest.readAsStringSync());
|
||||
if (map is Map<String, dynamic> && map["name"] is String) {
|
||||
return map["name"] as String;
|
||||
}
|
||||
} on FormatException {
|
||||
// fall through to the directory name
|
||||
}
|
||||
}
|
||||
return directory.uri.pathSegments.last;
|
||||
}
|
||||
|
||||
/// The installed skins, base first, in scan order.
|
||||
List<SkinInfo> get skins => List<SkinInfo>.unmodifiable(_skins);
|
||||
|
||||
SkinInfo? byName(String name) {
|
||||
for (final info in _skins) {
|
||||
if (info.name == name) return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Loads [info] on top of the bundled base skin.
|
||||
Future<ClassicSkin> loadSkin(SkinInfo info) async {
|
||||
final base = await loadBaseSkin();
|
||||
final directory = info.directory;
|
||||
if (directory == null) return base;
|
||||
return loadSkinFromDirectory(base, directory, name: info.name);
|
||||
}
|
||||
|
||||
/// Extracts a `.wsz` archive into the skins directory and returns its
|
||||
/// [SkinInfo]. [name] overrides the display name; otherwise the `skin.xml`
|
||||
/// id is used, falling back to [bytes]' suggested filename or a generic
|
||||
/// label.
|
||||
///
|
||||
/// Re-importing the same skin replaces its previous folder.
|
||||
Future<SkinInfo> importArchive(
|
||||
Uint8List bytes, {
|
||||
String? name,
|
||||
String? fileName,
|
||||
}) async {
|
||||
final archive = ZipDecoder().decodeBytes(bytes, verify: false);
|
||||
final resolvedName =
|
||||
name ?? skinDisplayName(archive) ?? _labelFromFileName(fileName);
|
||||
final folderName = _safeFolderName(resolvedName);
|
||||
|
||||
final target = Directory(
|
||||
_directory.uri.resolve("$folderName/").toFilePath(),
|
||||
);
|
||||
if (await target.exists()) {
|
||||
await target.delete(recursive: true);
|
||||
}
|
||||
await target.create(recursive: true);
|
||||
|
||||
for (final file in archive.files) {
|
||||
if (!file.isFile) continue;
|
||||
final key = file.name;
|
||||
if (key.contains("\\") || key.contains("/")) continue;
|
||||
await File(
|
||||
target.uri.resolve(key).toFilePath(),
|
||||
).writeAsBytes(file.content as List<int>, flush: true);
|
||||
}
|
||||
|
||||
final manifest = {
|
||||
"name": resolvedName,
|
||||
"importedAt": DateTime.now().toIso8601String(),
|
||||
};
|
||||
await File(
|
||||
target.uri.resolve(manifestName).toFilePath(),
|
||||
).writeAsString(jsonEncode(manifest), flush: true);
|
||||
|
||||
final info = SkinInfo(name: resolvedName, directory: target);
|
||||
_skins.removeWhere((s) => s.name == info.name);
|
||||
_skins.add(info);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
String _labelFromFileName(String? fileName) {
|
||||
if (fileName == null) return "Imported Skin";
|
||||
return fileName
|
||||
.split(RegExp(r"[\\/]"))
|
||||
.last
|
||||
.replaceAll(RegExp(r"\.(wsz|zip)$", caseSensitive: false), "");
|
||||
}
|
||||
|
||||
/// Keeps folder names filesystem-safe while staying human readable.
|
||||
String _safeFolderName(String name) {
|
||||
final cleaned = name.trim().replaceAll(RegExp(r"[^A-Za-z0-9_. -]"), "_");
|
||||
if (cleaned.isEmpty) return "skin";
|
||||
return cleaned;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
name: dartamp_skin
|
||||
description: >-
|
||||
Classic Winamp skin loading, decoding, and rendering helpers used by the
|
||||
dartamp desktop app. Kept as a separate package so a future "modern" skin
|
||||
codepath can live alongside the classic one.
|
||||
version: 0.1.0
|
||||
publish_to: "none"
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
dependencies:
|
||||
archive: ^4.0.9
|
||||
flutter:
|
||||
sdk: flutter
|
||||
path: ^1.9.0
|
||||
|
||||
dev_dependencies:
|
||||
arcane_analysis: ^1.0.5
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/skins/base/
|
||||
@@ -0,0 +1,24 @@
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test("loadBaseSkin decodes every bundled bitmap", () async {
|
||||
final skin = await loadBaseSkin();
|
||||
expect(skin.name, "Base");
|
||||
for (final name in bundledSkinFiles) {
|
||||
final image = skin.image(name);
|
||||
expect(image, isNotNull, reason: "$name should be loaded");
|
||||
expect(image!.width, greaterThan(0));
|
||||
}
|
||||
});
|
||||
|
||||
test("loadBaseSkin decodes MAIN.BMP via case-insensitive lookup", () async {
|
||||
final skin = await loadBaseSkin();
|
||||
final main = skin.image("MAIN.BMP");
|
||||
expect(main, isNotNull);
|
||||
expect(main!.width, 275);
|
||||
expect(main.height, 116);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:archive/archive.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
Uint8List _encode(Archive archive) =>
|
||||
Uint8List.fromList(ZipEncoder().encode(archive));
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp("skin_registry_test");
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
test("empty registry lists only the bundled base skin", () async {
|
||||
final registry = await SkinRegistry.load(tempDir);
|
||||
expect(registry.skins.map((s) => s.name), ["Base"]);
|
||||
expect(registry.skins.first.isBundled, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
"importArchive extracts files, writes a manifest, and reappears",
|
||||
() async {
|
||||
final archive = Archive();
|
||||
final numbers = File(
|
||||
"assets/skins/base/numbers.bmp",
|
||||
).readAsBytesSync();
|
||||
archive.addFile(
|
||||
ArchiveFile("numbers.bmp", numbers.length, numbers),
|
||||
);
|
||||
archive.addFile(
|
||||
ArchiveFile(
|
||||
"pledit.txt",
|
||||
"pledit.txt".length,
|
||||
utf8.encode("pledit.txt"),
|
||||
),
|
||||
);
|
||||
archive.addFile(
|
||||
ArchiveFile(
|
||||
"skin.xml",
|
||||
'<skin id="My Cool Skin" />'.length,
|
||||
utf8.encode('<skin id="My Cool Skin" />'),
|
||||
),
|
||||
);
|
||||
final bytes = _encode(archive);
|
||||
|
||||
final registry = await SkinRegistry.load(tempDir);
|
||||
final info = await registry.importArchive(
|
||||
bytes,
|
||||
fileName: "myskin.wsz",
|
||||
);
|
||||
|
||||
expect(info.name, "My Cool Skin");
|
||||
expect(info.isBundled, isFalse);
|
||||
|
||||
final folder = info.directory!;
|
||||
expect(File("${folder.path}/numbers.bmp").existsSync(), isTrue);
|
||||
expect(File("${folder.path}/manifest.json").existsSync(), isTrue);
|
||||
final manifest =
|
||||
jsonDecode(
|
||||
File("${folder.path}/manifest.json").readAsStringSync(),
|
||||
)
|
||||
as Map<String, dynamic>;
|
||||
expect(manifest["name"], "My Cool Skin");
|
||||
|
||||
final reloaded = await SkinRegistry.load(tempDir);
|
||||
expect(reloaded.skins.map((s) => s.name), contains("My Cool Skin"));
|
||||
},
|
||||
);
|
||||
|
||||
test("loadSkin overrides the base with imported bitmaps", () async {
|
||||
final archive = Archive();
|
||||
final numbers = File(
|
||||
"assets/skins/base/numbers.bmp",
|
||||
).readAsBytesSync();
|
||||
archive.addFile(
|
||||
ArchiveFile("numbers.bmp", numbers.length, numbers),
|
||||
);
|
||||
final bytes = _encode(archive);
|
||||
|
||||
final registry = await SkinRegistry.load(tempDir);
|
||||
final info = await registry.importArchive(
|
||||
bytes,
|
||||
name: "Test",
|
||||
fileName: "test.wsz",
|
||||
);
|
||||
|
||||
final skin = await registry.loadSkin(info);
|
||||
expect(skin.name, "Test");
|
||||
expect(skin.image("numbers.bmp")!.width, greaterThan(0));
|
||||
// Files the archive omits fall back to the base skin.
|
||||
expect(skin.image("main.bmp"), isNotNull);
|
||||
});
|
||||
|
||||
test("folder names are sanitized", () async {
|
||||
final archive = Archive();
|
||||
final bytes = _encode(archive);
|
||||
final registry = await SkinRegistry.load(tempDir);
|
||||
final info = await registry.importArchive(
|
||||
bytes,
|
||||
name: "Weird/Name: *?",
|
||||
fileName: "weird.wsz",
|
||||
);
|
||||
expect(info.directory!.uri.pathSegments.last, isNot(contains(":")));
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import "dart:io";
|
||||
import "dart:ui" as ui;
|
||||
|
||||
import "package:dartamp/src/skin/bmp_decoder.dart";
|
||||
import "package:dartamp/src/skin/text_font.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
Future<TextFont> _loadFont() async {
|
||||
@@ -3,7 +3,7 @@
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:dartamp/src/skin/bmp_decoder.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
|
||||
Future<void> main() async {
|
||||
final out = Directory("tool/preview");
|
||||
@@ -3,7 +3,7 @@
|
||||
import "dart:io";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:dartamp/src/skin/bmp_decoder.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
for (final name in args) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:dartamp/src/skin/bmp_decoder.dart";
|
||||
import "package:dartamp_skin/dartamp_skin.dart";
|
||||
|
||||
void main(List<String> args) {
|
||||
final file = File(args[0]);
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 505 B After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@@ -7,11 +7,13 @@ environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
dependencies:
|
||||
archive: ^4.0.9
|
||||
dartamp_skin:
|
||||
path: packages/dartamp_skin
|
||||
desktop_multi_window: ^0.3.0
|
||||
file_picker: ^11.0.3
|
||||
flutter:
|
||||
sdk: flutter
|
||||
path_provider: ^2.1.5
|
||||
shared_preferences: ^2.5.5
|
||||
window_manager: ^0.5.2
|
||||
|
||||
@@ -24,5 +26,3 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/skins/base/
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import "package:dartamp/main.dart";
|
||||
import "package:dartamp/src/skin/skin_manager.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_driver/driver_extension.dart";
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
enableFlutterDriverExtension();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final skinManager = await SkinManager.load();
|
||||
runApp(
|
||||
const DartampApp(
|
||||
DartampApp(
|
||||
initialWindowShade: false,
|
||||
initialAlwaysOnTop: false,
|
||||
initialEqOpen: false,
|
||||
initialPlOpen: false,
|
||||
initialUiScale: 1.0,
|
||||
skinManager: skinManager,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||