Files
SpotiFLAC-Mobile/lib/widgets/player_lyrics_section.dart
T
zarzet dbee3d56cb feat(player): search lyrics online for files without them
The empty lyrics page offers Search online, which asks the enabled lyrics
providers in their configured order. The result is shown in the player
only and never written to the file; a late result is dropped after the
track changes, and a miss says so.
2026-10-18 19:24:39 +07:00

144 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/controllers/player_metadata_controller.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/utils/lyrics_parser.dart';
import 'package:spotiflac_android/utils/playback_seek_preview.dart';
import 'package:spotiflac_android/widgets/app_icon.dart';
import 'package:spotiflac_android/widgets/app_loading_indicator.dart';
import 'package:spotiflac_android/widgets/expressive_button.dart';
import 'package:spotiflac_android/widgets/player_lyrics.dart';
import 'package:spotiflac_android/widgets/lyrics_text_scale.dart';
class PlayerLyricsSection extends ConsumerWidget {
const PlayerLyricsSection({
super.key,
required this.metadata,
required this.seekPreview,
required this.colorScheme,
required this.isActive,
this.options,
this.onShareLyrics,
});
final PlayerMetadataController metadata;
final PlaybackSeekPreview seekPreview;
final ColorScheme colorScheme;
final bool isActive;
final Widget? options;
/// Long-pressing lyrics opens sharing, preselecting a timed line.
final ValueChanged<LyricLine?>? onShareLyrics;
@override
Widget build(BuildContext context, WidgetRef ref) {
final visibility = ref.watch(
settingsProvider.select(
(settings) =>
(settings.playerShowPronunciation, settings.playerShowTranslation),
),
);
return ListenableBuilder(
listenable: metadata,
builder: (context, _) => _buildLyricsSection(context, visibility),
);
}
Widget _buildLyricsSection(BuildContext context, (bool, bool) visibility) {
if (metadata.loading || metadata.searchingOnline) {
return const Center(child: AppLoadingIndicator());
}
if (metadata.lyrics.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppIcon(
Icons.lyrics_outlined,
size: 40,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(height: 12),
Text(
metadata.onlineSearchMissed
? context.l10n.nowPlayingNoLyricsOnline
: context.l10n.nowPlayingNoLyrics,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
if (metadata.canSearchOnline) ...[
const SizedBox(height: 16),
ExpressiveButton(
tonal: true,
onPressed: metadata.searchOnline,
icon: const Icon(Icons.manage_search),
child: Text(context.l10n.nowPlayingSearchLyricsOnline),
),
],
],
),
);
}
if (metadata.lyrics.synced) {
return Stack(
fit: StackFit.expand,
children: [
SyncedLyricsView(
lyrics: metadata.lyrics,
seekPreview: seekPreview,
credits: _lyricsCredits(),
colorScheme: colorScheme,
isActive: isActive,
showPronunciation: visibility.$1,
showTranslation: visibility.$2,
onLongPressLine: onShareLyrics,
),
if (options != null)
Positioned(
left: context.isMornye ? mornyeLyricsContentInset : 24,
bottom: 8,
child: options!,
),
],
);
}
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
onLongPress: onShareLyrics == null
? null
: () {
HapticFeedback.mediumImpact();
onShareLyrics!(null);
},
child: LyricsTextScale(
child: Text(
metadata.lyrics.plainText,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
height: 1.6,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
),
),
?_lyricsCredits(),
],
),
);
}
LyricsCredits? _lyricsCredits() {
final (:writers, :provider) = metadata.credits;
if (writers == null && provider == null) return null;
return LyricsCredits(writers: writers, provider: provider);
}
}