Alternative tracks

This commit is contained in:
LittleSheep 2024-08-29 00:33:59 +08:00
parent 3f41573f00
commit 2134500089
7 changed files with 178 additions and 12 deletions

View File

@ -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(),
)
],
),
);
}
}

View File

@ -10,8 +10,10 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/screens/player/queue.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/artist.dart';
import 'package:rhythm_box/services/audio_player/audio_player.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/widgets/auto_cache_image.dart';
import 'package:rhythm_box/services/audio_services/image.dart'; import 'package:rhythm_box/services/audio_services/image.dart';
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
@ -53,14 +55,6 @@ class _PlayerScreenState extends State<PlayerScreen> {
setState(() {}); 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; double? _draggingValue;
@override @override
@ -173,11 +167,11 @@ class _PlayerScreenState extends State<PlayerScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
_formatDuration(_durationCurrent), _durationCurrent.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12), style: GoogleFonts.robotoMono(fontSize: 12),
), ),
Text( Text(
_formatDuration(_durationTotal), _durationTotal.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12), style: GoogleFonts.robotoMono(fontSize: 12),
), ),
], ],
@ -302,7 +296,18 @@ class _PlayerScreenState extends State<PlayerScreen> {
child: TextButton.icon( child: TextButton.icon(
icon: const Icon(Icons.merge), icon: const Icon(Icons.merge),
label: const Text('Sources'), label: const Text('Sources'),
onPressed: () {}, onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => const SiblingTracksPopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
), ),
), ),
], ],

View File

@ -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);
}
}

View File

@ -249,7 +249,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
final query = SourcedTrack.getSearchTerm(track); final query = SourcedTrack.getSearchTerm(track);
final searchResults = await youtubeClient.search.search( final searchResults = await youtubeClient.search.search(
'$query - Topic', query,
// '$query - Topic',
filter: TypeFilters.video, filter: TypeFilters.video,
); );

View File

@ -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<SiblingTracks> createState() => _SiblingTracksState();
}
class _SiblingTracksState extends State<SiblingTracks> {
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<SourceInfo> 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();
}
},
);
},
),
);
}
}

View File

@ -366,6 +366,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
duration:
dependency: "direct main"
description:
name: duration
sha256: "13e5d20723c9c1dde8fb318cf86716d10ce294734e81e44ae1a817f3ae714501"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
encrypt: encrypt:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -85,6 +85,7 @@ dependencies:
scroll_to_index: ^3.0.1 scroll_to_index: ^3.0.1
animations: ^2.0.11 animations: ^2.0.11
flutter_animate: ^4.5.0 flutter_animate: ^4.5.0
duration: ^4.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: