Skip to content

Commit

Permalink
feat: Implement basic structure for the lyric display components
Browse files Browse the repository at this point in the history
  • Loading branch information
Losses committed Dec 14, 2024
1 parent 5801f0e commit d1a79e0
Show file tree
Hide file tree
Showing 23 changed files with 475 additions and 28 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions lib/config/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../utils/router/query_tracks_parameter.dart';
import '../routes/home.dart' as home;
import '../routes/mixes.dart' as mixes;
import '../routes/tracks.dart' as tracks;
import '../routes/lyrics.dart' as lyrics;
import '../routes/search.dart' as search;
import '../routes/welcome.dart' as welcome;
import '../routes/settings.dart' as settings;
Expand Down Expand Up @@ -106,8 +107,8 @@ final Map<String, WidgetBuilder> routes = {
'/settings/library_home': (context) => const settings.SettingsLibraryHome(),
'/settings/media_controller': (context) =>
const settings.SettingsMediaControllerPage(),
'/settings/laboratory': (context) =>
const settings.SettingsLaboratory(),
'/settings/laboratory': (context) => const settings.SettingsLaboratory(),
'/search': (context) => const search.SearchPage(),
'/cover_wall': (context) => const cover_wall.CoverWallPage(),
'/lyrics': (context) => const lyrics.LyricsPage(),
};
8 changes: 7 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -968,5 +968,11 @@
"considerPurchase": "Please consider purchasing a genuine license.",
"@considerPurchase": {
"description": "Notice in the about page, guiding user to purchase a license"
}
},
"lyrics": "Lyrics",
"@lyrics": {
"description": "Button prompt text for controlling playback, used to switch to the lyrics page."
},
"lyricsSubtitle": "Reveal the full lyrics of the song",
"@lyricsSubtitle": {}
}
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class _RuneState extends State<Rune> {
initialRoute: initialPath == null
? "/"
: cafeMode
? "/cover_wall"
? '/cover_wall'
: "/library",
navigatorKey: rootNavigatorKey,
onGenerateRoute: (settings) {
Expand Down
1 change: 1 addition & 0 deletions lib/routes/lyrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export '../screens/lyrics/lyrics.dart';
2 changes: 1 addition & 1 deletion lib/screens/cover_wall/cover_wall.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:fluent_ui/fluent_ui.dart';

import '../../widgets/navigation_bar/page_content_frame.dart';
import '../../screens/cover_wall/band_screen_cover_wall.dart';
import '../../providers/responsive_providers.dart';

import 'widgets/cover_wall_layout.dart';
import 'band_screen_cover_wall.dart';

class CoverWallPage extends StatefulWidget {
const CoverWallPage({super.key});
Expand Down
25 changes: 25 additions & 0 deletions lib/screens/lyrics/band_screen_lyrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:fluent_ui/fluent_ui.dart';

class BandScreenLyricsView extends StatefulWidget {
const BandScreenLyricsView({super.key});

@override
LibraryHomeListState createState() => LibraryHomeListState();
}

class LibraryHomeListState extends State<BandScreenLyricsView> {
@override
void dispose() {
super.dispose();
}

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Container();
}
}
106 changes: 106 additions & 0 deletions lib/screens/lyrics/lyrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'package:provider/provider.dart';
import 'package:fluent_ui/fluent_ui.dart';

import '../../utils/rune_log.dart';
import '../../utils/api/get_lyric_by_track_id.dart';
import '../../widgets/navigation_bar/page_content_frame.dart';
import '../../messages/all.dart';
import '../../providers/status.dart';
import '../../providers/responsive_providers.dart';

import 'band_screen_lyrics.dart';
import 'widgets/lyrics_layout.dart';

class LyricsPage extends StatefulWidget {
const LyricsPage({super.key});

@override
State<LyricsPage> createState() => _LyricsPageState();
}

class _LyricsPageState extends State<LyricsPage> {
int _cachedTrackId = -1;
Future<List<LyricContentLine>>? _lyric;

late PlaybackStatusProvider playbackStatus;

@override
void didChangeDependencies() {
super.didChangeDependencies();

playbackStatus =
Provider.of<PlaybackStatusProvider>(context, listen: false);

playbackStatus.addListener(_handlePlaybackStatusUpdate);
_handlePlaybackStatusUpdate();
}

@override
void dispose() {
super.dispose();
playbackStatus.removeListener(_handlePlaybackStatusUpdate);
}

_handlePlaybackStatusUpdate() {
if (_cachedTrackId != playbackStatus.playbackStatus.id) {
setState(() {
final id = playbackStatus.playbackStatus.id;
_cachedTrackId = id;
_lyric = getLyricByTrackId(id);
});
}
}

int _selectProgress(BuildContext context, PlaybackStatusProvider status) {
return (status.playbackStatus.progressSeconds * 1000).round();
}

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _lyric,
builder: (context, snapshot) {
if (snapshot.data == null) return Container();

for (final line in snapshot.data!) {
info$(line.sections.map((x) => x.content).join(""));
}

return Selector<PlaybackStatusProvider, int>(
selector: _selectProgress,
builder: (context, currentTimeMilliseconds, child) {
final List<int> activeLines = [];

for (final (index, line) in snapshot.data!.indexed) {
if (currentTimeMilliseconds > line.startTime &&
currentTimeMilliseconds < line.endTime) {
activeLines.add(index);
}
}

return DeviceTypeBuilder(
deviceType: const [
DeviceType.band,
DeviceType.dock,
DeviceType.zune,
DeviceType.tv
],
builder: (context, activeBreakpoint) {
if (activeBreakpoint == DeviceType.dock ||
activeBreakpoint == DeviceType.band) {
return const PageContentFrame(child: BandScreenLyricsView());
}

return LyricsLayout(
lyrics: snapshot.data!,
currentTimeMilliseconds: currentTimeMilliseconds,
activeLines: activeLines,
);
},
);
},
);
},
);
}
}
34 changes: 34 additions & 0 deletions lib/screens/lyrics/widgets/lyric_display.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:fluent_ui/fluent_ui.dart';

import '../../../messages/all.dart';

import 'lyric_line.dart';

class LyricsDisplay extends StatelessWidget {
final List<LyricContentLine> lyrics;
final int currentTimeMilliseconds;
final List<int> activeLines;

const LyricsDisplay({
super.key,
required this.lyrics,
required this.currentTimeMilliseconds,
required this.activeLines,
});

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: lyrics.length,
itemBuilder: (context, index) {
final line = lyrics[index];
return LyricLine(
key: ValueKey(index),
sections: line.sections,
currentTimeMilliseconds: currentTimeMilliseconds,
isActive: activeLines.contains(index),
);
},
);
}
}
73 changes: 73 additions & 0 deletions lib/screens/lyrics/widgets/lyric_line.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:fluent_ui/fluent_ui.dart';

import '../../../messages/all.dart';

class LyricLine extends StatelessWidget {
final List<LyricContentLineSection> sections;
final int currentTimeMilliseconds;
final bool isActive;

const LyricLine({
super.key,
required this.sections,
required this.currentTimeMilliseconds,
required this.isActive,
});

double calculateProgress() {
if (!isActive) return 0.0;

double totalDuration = 0;
double currentProgress = 0;

for (final section in sections) {
final duration = section.endTime - section.startTime;
totalDuration += duration;

if (currentTimeMilliseconds >= section.endTime) {
currentProgress += duration;
} else if (currentTimeMilliseconds > section.startTime) {
currentProgress += (currentTimeMilliseconds - section.startTime);
}
}

return totalDuration > 0 ? (currentProgress / totalDuration) : 0.0;
}

@override
Widget build(BuildContext context) {
final progress = calculateProgress();
final text = sections.map((s) => s.content).join("");
final theme = FluentTheme.of(context);

return Container(
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
child: Stack(
children: [
// Highlighten text
Text(
text,
style: TextStyle(
fontSize: 20,
color: theme.resources.textFillColorPrimary,
),
),
// Non-highlighten text
ClipRect(
child: Align(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Text(
text,
style: TextStyle(
fontSize: 18,
color: theme.resources.textFillColorPrimary.withAlpha(160),
),
),
),
),
],
),
);
}
}
Loading

0 comments on commit d1a79e0

Please sign in to comment.