Keyboard support on player

This commit is contained in:
2025-12-16 23:50:04 +08:00
parent 79e0c65f0b
commit c0a495484c

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:groovybox/data/db.dart' as db; import 'package:groovybox/data/db.dart' as db;
import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/lyrics_parser.dart';
@@ -37,70 +38,94 @@ class PlayerScreen extends HookConsumerWidget {
final path = Uri.decodeFull(Uri.parse(media.uri).path); final path = Uri.decodeFull(Uri.parse(media.uri).path);
final metadataAsync = ref.watch(trackMetadataProvider(path)); final metadataAsync = ref.watch(trackMetadataProvider(path));
return Scaffold( return Focus(
body: Stack( autofocus: true,
children: [ onKeyEvent: (node, event) {
// Main content (StreamBuilder) if (event is KeyDownEvent) {
Builder( if (event.logicalKey == LogicalKeyboardKey.space) {
builder: (context) { if (player.state.playing) {
if (isMobile) { player.pause();
return Padding( } else {
padding: EdgeInsets.only( player.play();
top: MediaQuery.of(context).padding.top + 64, }
), return KeyEventResult.handled;
child: _MobileLayout( } else if (event.logicalKey == LogicalKeyboardKey.bracketLeft) {
player.previous();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.bracketRight) {
player.next();
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
child: Scaffold(
body: Stack(
children: [
// Main content (StreamBuilder)
Builder(
builder: (context) {
if (isMobile) {
return Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 64,
),
child: _MobileLayout(
player: player,
tabController: tabController,
metadataAsync: metadataAsync,
media: media,
trackPath: path,
),
);
} else {
return _DesktopLayout(
player: player, player: player,
tabController: tabController,
metadataAsync: metadataAsync, metadataAsync: metadataAsync,
media: media, media: media,
trackPath: path, trackPath: path,
), );
); }
} else { },
return _DesktopLayout(
player: player,
metadataAsync: metadataAsync,
media: media,
trackPath: path,
);
}
},
),
// IconButton
Positioned(
top: MediaQuery.of(context).padding.top + 16,
left: 16,
child: IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
iconSize: 24,
), ),
), // IconButton
// TabBar (if mobile)
if (isMobile)
Positioned( Positioned(
top: MediaQuery.of(context).padding.top + 14, top: MediaQuery.of(context).padding.top + 16,
left: 54, left: 16,
right: 54, child: IconButton(
child: Padding( icon: const Icon(Icons.keyboard_arrow_down),
padding: const EdgeInsets.symmetric(horizontal: 24), onPressed: () => Navigator.of(context).pop(),
child: TabBar( padding: EdgeInsets.zero,
controller: tabController, iconSize: 24,
tabAlignment: TabAlignment.fill,
tabs: const [
Tab(text: 'Cover'),
Tab(text: 'Lyrics'),
],
dividerHeight: 0,
indicatorColor: Colors.transparent,
overlayColor: WidgetStatePropertyAll(Colors.transparent),
splashFactory: NoSplash.splashFactory,
),
), ),
), ),
_LyricsRefreshButton(trackPath: path), // TabBar (if mobile)
], if (isMobile)
Positioned(
top: MediaQuery.of(context).padding.top + 14,
left: 54,
right: 54,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TabBar(
controller: tabController,
tabAlignment: TabAlignment.fill,
tabs: const [
Tab(text: 'Cover'),
Tab(text: 'Lyrics'),
],
dividerHeight: 0,
indicatorColor: Colors.transparent,
overlayColor: WidgetStatePropertyAll(
Colors.transparent,
),
splashFactory: NoSplash.splashFactory,
),
),
),
_LyricsRefreshButton(trackPath: path),
],
),
), ),
); );
}, },
@@ -251,29 +276,38 @@ class _PlayerCoverArt extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return metadataAsync.when( return metadataAsync.when(
data: (meta) => Container( data: (meta) => Center(
decoration: BoxDecoration( child: AspectRatio(
color: Colors.grey[800], aspectRatio: 1,
borderRadius: BorderRadius.circular(24), child: Container(
boxShadow: [ decoration: BoxDecoration(
BoxShadow( color: Colors.grey[800],
color: Colors.black.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(24),
blurRadius: 20, boxShadow: [
offset: const Offset(0, 10), BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
image: meta.artBytes != null
? DecorationImage(
image: MemoryImage(meta.artBytes!),
fit: BoxFit.cover,
)
: null,
), ),
], child: meta.artBytes == null
image: meta.artBytes != null ? const Center(
? DecorationImage( child: Icon(
image: MemoryImage(meta.artBytes!), Icons.music_note,
fit: BoxFit.cover, size: 80,
) color: Colors.white54,
: null, ),
)
: null,
),
), ),
child: meta.artBytes == null
? const Center(
child: Icon(Icons.music_note, size: 80, color: Colors.white54),
)
: null,
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (_, _) => Container( error: (_, _) => Container(
@@ -994,7 +1028,7 @@ class _PlayerControls extends HookWidget {
), ),
), ),
loading: () => const SizedBox(height: 24), loading: () => const SizedBox(height: 24),
error: (_, __) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
), ),
], ],
), ),