✨ Alternative tracks
This commit is contained in:
		
							
								
								
									
										26
									
								
								lib/screens/player/siblings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/screens/player/siblings.dart
									
									
									
									
									
										Normal 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(),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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(() {});
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								lib/services/duration.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/services/duration.dart
									
									
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										99
									
								
								lib/widgets/player/sibling_tracks.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								lib/widgets/player/sibling_tracks.dart
									
									
									
									
									
										Normal 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();
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user