Rollback music player screen code

This commit is contained in:
2026-01-03 19:24:43 +08:00
parent 860c02fc8c
commit 79a68f316d

View File

@@ -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<Playlist>(
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<String>()
.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<void> _batchImportLyricsFromPaths(
BuildContext context,
WidgetRef ref,
List<String> 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',
),
),
);
}