💄 Curvy lyrics on desktop

This commit is contained in:
2025-12-15 00:26:57 +08:00
parent e7846734b0
commit 82226ede2f

View File

@@ -45,7 +45,7 @@ class PlayerScreen extends HookConsumerWidget {
if (isMobile) { if (isMobile) {
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 48, top: MediaQuery.of(context).padding.top + 64,
), ),
child: _MobileLayout( child: _MobileLayout(
player: player, player: player,
@@ -69,20 +69,21 @@ class PlayerScreen extends HookConsumerWidget {
), ),
// IconButton // IconButton
Positioned( Positioned(
top: MediaQuery.of(context).padding.top + 8, top: MediaQuery.of(context).padding.top + 16,
left: 8, left: 16,
child: IconButton( child: IconButton(
icon: const Icon(Icons.keyboard_arrow_down), icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
iconSize: 24,
), ),
), ),
// TabBar (if mobile) // TabBar (if mobile)
if (isMobile) if (isMobile)
Positioned( Positioned(
top: MediaQuery.of(context).padding.top + 8, top: MediaQuery.of(context).padding.top + 14,
left: 50, left: 54,
right: 50, right: 54,
child: TabBar( child: TabBar(
controller: tabController, controller: tabController,
tabAlignment: TabAlignment.fill, tabAlignment: TabAlignment.fill,
@@ -175,11 +176,13 @@ class _DesktopLayout extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Stack(
children: [ children: [
// Left Side: Cover + Controls // Left Side: Cover + Controls
Positioned.fill(
child: Row(
children: [
Expanded( Expanded(
flex: 1,
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480), constraints: const BoxConstraints(maxWidth: 480),
@@ -191,7 +194,9 @@ class _DesktopLayout extends StatelessWidget {
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(
maxWidth: 400,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: _PlayerCoverArt( child: _PlayerCoverArt(
@@ -213,11 +218,18 @@ class _DesktopLayout extends StatelessWidget {
), ),
), ),
), ),
// Right Side: Lyrics Expanded(child: const SizedBox.shrink()),
Expanded( ],
flex: 1, ),
),
// Overlaid Lyrics on the right
Positioned(
right: 0,
top: 0,
bottom: 0,
width: MediaQuery.sizeOf(context).width * 0.6,
child: Padding( child: Padding(
padding: EdgeInsets.all(32.0), padding: const EdgeInsets.symmetric(horizontal: 32),
child: _PlayerLyrics(trackPath: trackPath, player: player), child: _PlayerLyrics(trackPath: trackPath, player: player),
), ),
), ),
@@ -347,8 +359,14 @@ class _TimedLyricsView extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDesktop = MediaQuery.sizeOf(context).width > 640;
final listController = useMemoized(() => ListController(), []); final listController = useMemoized(() => ListController(), []);
final scrollController = useScrollController(); final scrollController = useScrollController();
final wheelScrollController = useMemoized(
() => FixedExtentScrollController(),
[],
);
final previousIndex = useState(-1); final previousIndex = useState(-1);
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
@@ -372,6 +390,15 @@ class _TimedLyricsView extends HookWidget {
if (currentIndex != previousIndex.value) { if (currentIndex != previousIndex.value) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
previousIndex.value = currentIndex; previousIndex.value = currentIndex;
if (isDesktop) {
if (wheelScrollController.hasClients) {
wheelScrollController.animateToItem(
currentIndex,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
);
}
} else {
listController.animateToItem( listController.animateToItem(
index: currentIndex, index: currentIndex,
scrollController: scrollController, scrollController: scrollController,
@@ -379,9 +406,70 @@ class _TimedLyricsView extends HookWidget {
duration: (_) => const Duration(milliseconds: 300), duration: (_) => const Duration(milliseconds: 300),
curve: (_) => Curves.easeOutCubic, curve: (_) => Curves.easeOutCubic,
); );
}
}); });
} }
if (isDesktop) {
return ListWheelScrollView.useDelegate(
controller: wheelScrollController,
itemExtent: 50,
perspective: 0.002,
offAxisFraction: 1.5,
squeeze: 1.0,
diameterRatio: 2,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
childCount: lyrics.lines.length,
builder: (context, index) {
final line = lyrics.lines[index];
final isActive = index == currentIndex;
return Align(
alignment: Alignment.centerRight,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width * 0.4,
),
child: InkWell(
onTap: () {
if (line.timeMs != null) {
player.seek(Duration(milliseconds: line.timeMs!));
}
},
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 32),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: Theme.of(context).textTheme.bodyLarge!
.copyWith(
fontSize: isActive ? 18 : 16,
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
color: isActive
? Theme.of(context).colorScheme.primary
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.left,
child: Text(
line.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
);
},
),
);
}
return SuperListView.builder( return SuperListView.builder(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 0.25 * MediaQuery.sizeOf(context).height, top: 0.25 * MediaQuery.sizeOf(context).height,
@@ -414,7 +502,7 @@ class _TimedLyricsView extends HookWidget {
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of( : Theme.of(
context, context,
).colorScheme.onSurface.withValues(alpha: 0.7), ).colorScheme.onSurface.withOpacity(0.7),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
child: Text(line.text, textAlign: TextAlign.center), child: Text(line.text, textAlign: TextAlign.center),
@@ -459,7 +547,7 @@ class _PlayerControls extends HookWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
loading: () => const SizedBox(height: 32), loading: () => const SizedBox(height: 32),
error: (_, __) => Text(Uri.parse(media.uri).pathSegments.last), error: (_, _) => Text(Uri.parse(media.uri).pathSegments.last),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
metadataAsync.when( metadataAsync.when(