From 79a68f316d4ea2788fd6f726bac178c37f3e9df9 Mon Sep 17 00:00:00 2001 From: liang-work Date: Sat, 3 Jan 2026 19:24:43 +0800 Subject: [PATCH] =?UTF-8?q?=E2=8F=AARollback=20music=20player=20screen=20c?= =?UTF-8?q?ode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui/screens/player_screen.dart | 384 +++--------------------------- 1 file changed, 27 insertions(+), 357 deletions(-) diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index d1b3e8a..25476bc 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -19,17 +19,14 @@ import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/providers/db_provider.dart'; import 'package:groovybox/providers/lrc_fetcher_provider.dart'; import 'package:groovybox/providers/settings_provider.dart'; -import 'package:groovybox/router.dart'; import 'package:groovybox/ui/widgets/mini_player.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:path/path.dart' as p; import 'package:styled_widget/styled_widget.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; -import 'package:window_manager/window_manager.dart'; enum ViewMode { cover, lyrics, queue } @@ -63,34 +60,6 @@ class PlayerScreen extends HookConsumerWidget { }, [defaultPlayerScreen]); final isMobile = MediaQuery.sizeOf(context).width <= 800; - // Window maximized state - final isMaximized = useState(false); - - // Update window maximized state - useEffect(() { - if (isDesktopPlatform()) { - windowManager.isMaximized().then((value) { - isMaximized.value = value; - }); - } - return null; - }, []); - - // Set system UI overlay style to ensure status bar is visible - useEffect(() { - // Remove edgeToEdge mode to show system title bar like main screen - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); - SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.light, - systemNavigationBarColor: Colors.transparent, -systemNavigationBarIconBrightness: Brightness.light, - )); - return () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); - }; - }, []); - return StreamBuilder( stream: player.stream.playlist, initialData: player.state.playlist, @@ -98,7 +67,7 @@ systemNavigationBarIconBrightness: Brightness.light, final index = snapshot.data?.index ?? 0; final medias = snapshot.data?.medias ?? []; if (medias.isEmpty || index < 0 || index >= medias.length) { -return const Center(child: Text('No media selected')); + return const Center(child: Text('No media selected')); } final media = medias[index]; @@ -173,244 +142,7 @@ return const Center(child: Text('No media selected')); } return KeyEventResult.ignored; }, - child: isDesktopPlatform() ? Material( - color: Theme.of(context).colorScheme.surface, - child: Stack( - fit: StackFit.expand, - children: [ - // Main content - Positioned.fill( - child: Scaffold( - backgroundColor: Colors.transparent, - body: ClipRect( - child: Stack( - children: [ - ...background != null ? [background] : [], - // Main content (StreamBuilder) - Builder( - builder: (context) { - return Padding( - padding: EdgeInsets.only( - top: devicePadding.top + 32, // Account for custom title bar - ), - child: isMobile ? _MobileLayout( - player: player, - viewMode: viewMode, - media: media, - trackPath: media.uri, - ) : _DesktopLayout( - player: player, - viewMode: viewMode, - media: media, - trackPath: media.uri, - ), - ); - }, - ), - // Back button - Positioned( - top: devicePadding.top + 48, // Adjusted for custom title bar - left: 16, - child: IconButton( - icon: const Icon(Symbols.keyboard_arrow_down), - onPressed: () => Navigator.of(context).pop(), - padding: EdgeInsets.zero, - iconSize: 24, - ), - ), - // view control button - _ViewToggleButton(viewMode: viewMode), - ], - ), - ), - ), - ), - // Custom title bar - Positioned( - top: devicePadding.top, - left: 0, - right: 0, - child: GestureDetector( - onPanStart: (details) { - windowManager.startDragging(); - }, - child: Container( - height: 32, - color: Theme.of(context).colorScheme.surfaceContainer, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Row( - children: [ - Image.asset( - Theme.of(context).brightness == Brightness.dark - ? 'assets/images/icon-dark.png' - : 'assets/images/icon.jpg', - width: 20, - height: 20, - ), - const SizedBox(width: 8), - Text( - 'GroovyBox', - textAlign: TextAlign.start, - ), - ], - ).padding(horizontal: 12, vertical: 5), - ), - IconButton( - icon: const Icon(Symbols.settings), - onPressed: () { - if (isMaximized.value) { - windowManager.restore(); - isMaximized.value = false; - } - ref.read(routerProvider).push(AppRoutes.settings); - }, - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), - // Import button - NEW - IconButton( - icon: const Icon(Symbols.add_circle), - onPressed: () async { - if (isMaximized.value) { - windowManager.restore(); - isMaximized.value = false; - } - final result = await FilePicker.platform.pickFiles( - type: FileType.any, - allowMultiple: true, - ); - if (result != null && result.files.isNotEmpty) { - final paths = result.files - .map((f) => f.path) - .whereType() - .toList(); - if (paths.isNotEmpty) { - // Separate audio and lyrics files - final audioPaths = paths.where((path) { - final ext = p - .extension(path) - .toLowerCase() - .replaceFirst('.', ''); - return const [ - 'mp3', - 'm4a', - 'wav', - 'flac', - 'aac', - 'ogg', - 'wma', - 'm4p', - 'aiff', - 'au', - 'dss', - ].contains(ext); - }).toList(); - final lyricsPaths = paths.where((path) { - final ext = p - .extension(path) - .toLowerCase() - .replaceFirst('.', ''); - return const ['lrc', 'srt', 'txt'].contains(ext); - }).toList(); - - // Import tracks if any - if (audioPaths.isNotEmpty) { - await ref.read(trackRepositoryProvider.notifier).importFiles(audioPaths); - } - - // Import lyrics if any - if (!context.mounted) return; - if (lyricsPaths.isNotEmpty) { - await _batchImportLyricsFromPaths( - context, - ref, - lyricsPaths, - ); - } - } - } - }, - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - tooltip: 'Import Songs', - ), - // Page actions - IconButton( - icon: const Icon(Symbols.home), - onPressed: () { - if (isMaximized.value) { - windowManager.restore(); - isMaximized.value = false; - } - WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(routerProvider).go(AppRoutes.library); - }); - }, - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - tooltip: 'Home', - ), - // Window controls - IconButton( - icon: const Icon(Symbols.minimize), - onPressed: () => windowManager.minimize(), - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), - IconButton( - icon: Icon( - isMaximized.value - ? Symbols.fullscreen_exit - : Symbols.fullscreen, - ), - onPressed: () async { - if (await windowManager.isMaximized()) { - windowManager.restore(); - isMaximized.value = false; - } else { - windowManager.maximize(); - isMaximized.value = true; - } - }, - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), - IconButton( - icon: const Icon(Symbols.close), - onPressed: () => windowManager.close(), - iconSize: 16, - padding: EdgeInsets.all(8), - constraints: BoxConstraints(), - color: Theme.of(context).iconTheme.color, - ), - ], - ), - ), - ), - ), - ], - ), - ) : Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - scrolledUnderElevation: 0, - ), + child: Scaffold( body: ClipRect( child: Stack( children: [ @@ -418,22 +150,35 @@ return const Center(child: Text('No media selected')); // Main content (StreamBuilder) Builder( builder: (context) { - return Padding( - padding: EdgeInsets.only( - top: devicePadding.top + 40, - ), - child: _MobileLayout( + if (isMobile) { + return Padding( + padding: EdgeInsets.only( + top: + devicePadding.top + + 40 + + (isDesktopPlatform() ? 28 : 0), + ), + child: _MobileLayout( + player: player, + viewMode: viewMode, + media: media, + trackPath: media.uri, + ), + ); + } else { + return _DesktopLayout( player: player, viewMode: viewMode, media: media, trackPath: media.uri, - ), - ); + ); + } }, ), - // Back button for mobile platforms + // IconButton Positioned( - top: devicePadding.top + 16, + top: + devicePadding.top + 16 + (isDesktopPlatform() ? 28 : 0), left: 16, child: IconButton( icon: const Icon(Symbols.keyboard_arrow_down), @@ -442,37 +187,10 @@ return const Center(child: Text('No media selected')); iconSize: 24, ), ), - // Home button for mobile platforms - Positioned( - top: devicePadding.top + 16, - left: 56, - child: IconButton( - icon: const Icon(Symbols.home), - onPressed: () { - ref.read(routerProvider).go(AppRoutes.library); - }, - padding: EdgeInsets.zero, - iconSize: 24, - ), - ), - // Library button for mobile platforms - Positioned( - top: devicePadding.top + 16, - left: 96, - child: IconButton( - icon: const Icon(Symbols.library_music), - onPressed: () { - ref.read(routerProvider).go(AppRoutes.library); - }, - padding: EdgeInsets.zero, - iconSize: 24, - ), - ), - // view control button _ViewToggleButton(viewMode: viewMode), ], ), -), + ), ), ); }, @@ -532,6 +250,7 @@ class _PlayerCoverControlsPanel extends StatelessWidget { required this.media, required this.trackPath, }); + @override Widget build(BuildContext context) { return ConstrainedBox( @@ -731,6 +450,7 @@ class _LyricsView extends StatelessWidget { class _PlayerCoverArt extends StatelessWidget { final TrackMetadata? currentMetadata; + const _PlayerCoverArt({required this.currentMetadata}); @override @@ -2539,53 +2259,3 @@ String _formatTimestamp(int milliseconds) { (duration.inMilliseconds % 1000) ~/ 10; // Show centiseconds return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}.${millisecondsPart.toString().padLeft(2, '0')}'; } - -Future _batchImportLyricsFromPaths( - BuildContext context, - WidgetRef ref, - List lyricsPaths, -) async { - if (lyricsPaths.isEmpty) return; - - final repo = ref.read(trackRepositoryProvider.notifier); - final tracks = await repo.getAllTracks(); - - int matched = 0; - int notMatched = 0; - - for (final path in lyricsPaths) { - final file = File(path); - final content = await file.readAsString(); - final filename = p.basename(path); - - // Get basename without extension for matching - final baseName = filename - .replaceAll(RegExp(r'\.(lrc|srt|txt)$', caseSensitive: false), '') - .toLowerCase(); - - // Try to find a matching track by title - final matchingTrack = tracks.where((t) { - final trackTitle = t.title.toLowerCase(); - return trackTitle == baseName || - trackTitle.contains(baseName) || - baseName.contains(trackTitle); - }).firstOrNull; - - if (matchingTrack != null) { - final lyricsData = LyricsParser.parse(content, filename); - await repo.updateLyrics(matchingTrack.id, lyricsData.toJsonString()); - matched++; - } else { - notMatched++; - } - } - - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Batch import complete: $matched matched, $notMatched not matched', - ), - ), - ); -}