⏪Rollback music player screen code
This commit is contained in:
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user