From 2134500089bcd3583697c35d7ce93adefa942d11 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 29 Aug 2024 00:33:59 +0800 Subject: [PATCH] :sparkles: Alternative tracks --- lib/screens/player/siblings.dart | 26 +++++ lib/screens/player/view.dart | 27 ++--- lib/services/duration.dart | 26 +++++ .../sourced_track/sources/youtube.dart | 3 +- lib/widgets/player/sibling_tracks.dart | 99 +++++++++++++++++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 lib/screens/player/siblings.dart create mode 100644 lib/services/duration.dart create mode 100644 lib/widgets/player/sibling_tracks.dart diff --git a/lib/screens/player/siblings.dart b/lib/screens/player/siblings.dart new file mode 100644 index 0000000..e1c7911 --- /dev/null +++ b/lib/screens/player/siblings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/widgets/player/sibling_tracks.dart'; + +class SiblingTracksPopup extends StatelessWidget { + const SiblingTracksPopup({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.85, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Alternative Sources', + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + const Expanded( + child: SiblingTracks(), + ) + ], + ), + ); + } +} diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index 67d3594..2bc4f79 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -10,8 +10,10 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:media_kit/media_kit.dart'; import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/screens/player/queue.dart'; +import 'package:rhythm_box/screens/player/siblings.dart'; import 'package:rhythm_box/services/artist.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; +import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/services/audio_services/image.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; @@ -53,14 +55,6 @@ class _PlayerScreenState extends State { setState(() {}); } - String _formatDuration(Duration duration) { - String negativeSign = duration.isNegative ? '-' : ''; - String twoDigits(int n) => n.toString().padLeft(2, '0'); - String twoDigitMinutes = twoDigits(duration.inMinutes.abs()); - String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs()); - return '$negativeSign$twoDigitMinutes:$twoDigitSeconds'; - } - double? _draggingValue; @override @@ -173,11 +167,11 @@ class _PlayerScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - _formatDuration(_durationCurrent), + _durationCurrent.toHumanReadableString(), style: GoogleFonts.robotoMono(fontSize: 12), ), Text( - _formatDuration(_durationTotal), + _durationTotal.toHumanReadableString(), style: GoogleFonts.robotoMono(fontSize: 12), ), ], @@ -302,7 +296,18 @@ class _PlayerScreenState extends State { child: TextButton.icon( icon: const Icon(Icons.merge), label: const Text('Sources'), - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + context: context, + builder: (context) => const SiblingTracksPopup(), + ).then((_) { + if (mounted) { + setState(() {}); + } + }); + }, ), ), ], diff --git a/lib/services/duration.dart b/lib/services/duration.dart new file mode 100644 index 0000000..08c2b25 --- /dev/null +++ b/lib/services/duration.dart @@ -0,0 +1,26 @@ +extension DurationToHumanReadableString on Duration { + String toHumanReadableString({padZero = true}) { + final mm = inMinutes + .remainder(60) + .toString() + .padLeft(2, !padZero && inHours == 0 ? '' : '0'); + final ss = inSeconds.remainder(60).toString().padLeft(2, '0'); + + if (inHours > 0) { + final hh = inHours.toString().padLeft(2, !padZero ? '' : '0'); + return '$hh:$mm:$ss'; + } + + return '$mm:$ss'; + } +} + +extension ParseDuration on Duration { + static Duration fromString(String duration) { + final parts = duration.split(':').reversed.toList(); + final seconds = int.parse(parts[0]); + final minutes = parts.length > 1 ? int.parse(parts[1]) : 0; + final hours = parts.length > 2 ? int.parse(parts[2]) : 0; + return Duration(hours: hours, minutes: minutes, seconds: seconds); + } +} diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 1310092..712bd95 100755 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -249,7 +249,8 @@ class YoutubeSourcedTrack extends SourcedTrack { final query = SourcedTrack.getSearchTerm(track); final searchResults = await youtubeClient.search.search( - '$query - Topic', + query, + // '$query - Topic', filter: TypeFilters.video, ); diff --git a/lib/widgets/player/sibling_tracks.dart b/lib/widgets/player/sibling_tracks.dart new file mode 100644 index 0000000..fff5024 --- /dev/null +++ b/lib/widgets/player/sibling_tracks.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/services/duration.dart'; +import 'package:rhythm_box/services/server/active_sourced_track.dart'; +import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; +import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; +import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; +import 'package:rhythm_box/widgets/auto_cache_image.dart'; +import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; + +class SiblingTracks extends StatefulWidget { + const SiblingTracks({super.key}); + + @override + State createState() => _SiblingTracksState(); +} + +class _SiblingTracksState extends State { + late final QueryingTrackInfoProvider _query = Get.find(); + late final ActiveSourcedTrackProvider _activeSource = Get.find(); + late final AudioPlayerProvider _playback = Get.find(); + + get _activeTrack => + _activeSource.state.value ?? _playback.state.value.activeTrack; + + List get _siblings => !_query.isQueryingTrackInfo.value + ? [ + (_activeTrack as SourcedTrack).sourceInfo, + ..._activeSource.state.value!.siblings, + ] + : []; + + final sourceInfoToLabelMap = { + YoutubeSourceInfo: 'YouTube', + PipedSourceInfo: 'Piped', + }; + + @override + Widget build(BuildContext context) { + return Obx( + () => ListView.builder( + itemCount: _siblings.length, + itemBuilder: (context, idx) { + final item = _siblings[idx]; + final src = sourceInfoToLabelMap[item.runtimeType]; + return ListTile( + title: Text( + item.title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: AutoCacheImage( + item.thumbnail, + height: 64, + width: 64, + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + trailing: Text( + item.duration.toHumanReadableString(), + style: GoogleFonts.robotoMono(), + ), + subtitle: Row( + children: [ + if (src != null) Text(src), + Expanded( + child: Text( + ' ยท ${item.artist}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + enabled: !_query.isQueryingTrackInfo.value, + tileColor: !_query.isQueryingTrackInfo.value && + item.id == (_activeTrack as SourcedTrack).sourceInfo.id + ? Theme.of(context).colorScheme.secondaryContainer + : null, + onTap: () { + if (!_query.isQueryingTrackInfo.value && + item.id != (_activeTrack as SourcedTrack).sourceInfo.id) { + _activeSource.swapSibling(item); + Navigator.of(context).pop(); + } + }, + ); + }, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 625dbcc..a462b7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -366,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + duration: + dependency: "direct main" + description: + name: duration + sha256: "13e5d20723c9c1dde8fb318cf86716d10ce294734e81e44ae1a817f3ae714501" + url: "https://pub.dev" + source: hosted + version: "4.0.3" encrypt: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3ee5794..186ce8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,7 @@ dependencies: scroll_to_index: ^3.0.1 animations: ^2.0.11 flutter_animate: ^4.5.0 + duration: ^4.0.3 dev_dependencies: flutter_test: