diff --git a/assets/jiosaavn.png b/assets/jiosaavn.png new file mode 100644 index 00000000..4d2d46e4 Binary files /dev/null and b/assets/jiosaavn.png differ diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index d7149834..2587800e 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -34,6 +34,7 @@ class Assets { AssetGenImage('assets/bengali-patterns-bg.jpg'); static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); + static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png'); static const AssetGenImage likedTracks = AssetGenImage('assets/liked-tracks.jpg'); static const AssetGenImage placeholder = @@ -76,6 +77,7 @@ class Assets { bengaliPatternsBg, branding, emptyBox, + jiosaavn, likedTracks, placeholder, spotubeHeroBanner, diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 00010aae..c6acd669 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -109,4 +109,5 @@ abstract class SpotubeIcons { static const normalize = FeatherIcons.barChart2; static const wikipedia = SimpleIcons.wikipedia; static const discord = SimpleIcons.discord; + static const youtube = SimpleIcons.youtube; } diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index cf1429b9..181c363a 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart' hide Offset; +import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -19,10 +20,28 @@ import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; +import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +final sourceInfoToIconMap = { + YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), + JioSaavnSourceInfo: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + image: DecorationImage( + image: Assets.jiosaavn.provider(), + fit: BoxFit.cover, + ), + ), + ), + PipedSourceInfo: const Icon(SpotubeIcons.piped), +}; + class SiblingTracksSheet extends HookConsumerWidget { final bool floating; const SiblingTracksSheet({ @@ -64,17 +83,34 @@ class SiblingTracksSheet extends HookConsumerWidget { return []; } - final results = await youtubeClient.search.search(searchTerm.trim()); + final resultsYt = await youtubeClient.search.search(searchTerm.trim()); + final resultsJioSaavn = + await jiosaavnClient.search.songs(searchTerm.trim()); - return await Future.wait( - results.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { + final searchResults = await Future.wait([ + ...resultsJioSaavn.results.mapIndexed((i, song) async { + final siblingType = JioSaavnSourcedTrack.toSiblingType(song); + return siblingType.info; + }), + ...resultsYt + .map(YoutubeVideoInfo.fromVideo) + .mapIndexed((i, video) async { final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); return siblingType.info; }), - ); + ]); + final activeSourceInfo = + (playlist.activeTrack! as SourcedTrack).sourceInfo; + return searchResults + ..removeWhere((element) => element.id == activeSourceInfo.id) + ..insert( + 0, + activeSourceInfo, + ); }, [ searchTerm, searchMode.value, + playlist.activeTrack, ]); final siblings = useMemoized( @@ -104,6 +140,7 @@ class SiblingTracksSheet extends HookConsumerWidget { final itemBuilder = useCallback( (SourceInfo sourceInfo) { + final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; return ListTile( title: Text(sourceInfo.title), leading: Padding( @@ -118,7 +155,12 @@ class SiblingTracksSheet extends HookConsumerWidget { borderRadius: BorderRadius.circular(5), ), trailing: Text(sourceInfo.duration.toHumanReadableString()), - subtitle: Text(sourceInfo.artist), + subtitle: Row( + children: [ + if (icon != null) icon, + Text(" • ${sourceInfo.artist}"), + ], + ), enabled: playlist.isFetching != true, selected: playlist.isFetching != true && sourceInfo.id == @@ -137,7 +179,7 @@ class SiblingTracksSheet extends HookConsumerWidget { [playlist.isFetching, playlist.activeTrack, siblings], ); - var mediaQuery = MediaQuery.of(context); + final mediaQuery = MediaQuery.of(context); return SafeArea( child: ClipRRect( borderRadius: borderRadius, diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart index 48ce1cbd..e47ee6bd 100644 --- a/lib/services/sourced_track/enums.dart +++ b/lib/services/sourced_track/enums.dart @@ -15,4 +15,4 @@ enum SourceQualities { low, } -typedef SiblingType = ({SourceInfo info, SourceMap? source}); +typedef SiblingType = ({T info, SourceMap? source}); diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index a447b0c1..281be998 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -12,6 +12,19 @@ import 'package:spotube/extensions/string.dart'; final jiosaavnClient = JioSaavnClient(); +class JioSaavnSourceInfo extends SourceInfo { + JioSaavnSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class JioSaavnSourcedTrack extends SourcedTrack { JioSaavnSourcedTrack({ required super.ref, @@ -70,7 +83,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { static SiblingType toSiblingType(SongResponse result) { final SiblingType sibling = ( - info: SourceInfo( + info: JioSaavnSourceInfo( artist: [ result.primaryArtists, if (result.featuredArtists.isNotEmpty) ", ", @@ -155,12 +168,16 @@ class JioSaavnSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 0778a7cf..f9e4368d 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -22,6 +22,19 @@ final pipedProvider = Provider( }, ); +class PipedSourceInfo extends SourceInfo { + PipedSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class PipedSourcedTrack extends SourcedTrack { PipedSourcedTrack({ required super.ref, @@ -71,7 +84,7 @@ class PipedSourcedTrack extends SourcedTrack { ref: ref, siblings: [], source: toSourceMap(manifest), - sourceInfo: SourceInfo( + sourceInfo: PipedSourceInfo( id: manifest.id, artist: manifest.uploader, artistUrl: manifest.uploaderUrl, @@ -122,7 +135,7 @@ class PipedSourcedTrack extends SourcedTrack { } final SiblingType sibling = ( - info: SourceInfo( + info: PipedSourceInfo( id: item.id, artist: item.channelName, artistUrl: "https://www.youtube.com/${item.channelId}", @@ -233,12 +246,16 @@ class PipedSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 2bcd6e3e..c4105d75 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -17,6 +17,19 @@ final officialMusicRegex = RegExp( caseSensitive: false, ); +class YoutubeSourceInfo extends SourceInfo { + YoutubeSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class YoutubeSourcedTrack extends SourcedTrack { YoutubeSourcedTrack({ required super.source, @@ -64,7 +77,7 @@ class YoutubeSourcedTrack extends SourcedTrack { ref: ref, siblings: [], source: toSourceMap(manifest), - sourceInfo: SourceInfo( + sourceInfo: YoutubeSourceInfo( id: item.id.value, artist: item.author, artistUrl: "https://www.youtube.com/channel/${item.channelId}", @@ -117,7 +130,7 @@ class YoutubeSourcedTrack extends SourcedTrack { } final SiblingType sibling = ( - info: SourceInfo( + info: YoutubeSourceInfo( id: item.id, artist: item.channelName, artistUrl: "https://www.youtube.com/channel/${item.channelId}", @@ -217,12 +230,16 @@ class YoutubeSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo);