diff --git a/assets/album-placeholder.png b/assets/album-placeholder.png new file mode 100644 index 00000000..bcb25969 Binary files /dev/null and b/assets/album-placeholder.png differ diff --git a/assets/user-placeholder.png b/assets/user-placeholder.png new file mode 100644 index 00000000..cb2325ff Binary files /dev/null and b/assets/user-placeholder.png differ diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index df792eab..6b2459c4 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -148,20 +148,38 @@ class Sidebar extends HookConsumerWidget { child: CircularProgressIndicator(), ) else if (data != null) - Row( - children: [ - CircleAvatar( - backgroundImage: - CachedNetworkImageProvider(avatarImg), - ), - const SizedBox(width: 10), - Text( - data.displayName ?? "Guest", - style: const TextStyle( - fontWeight: FontWeight.bold, + Flexible( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + CircleAvatar( + backgroundImage: + CachedNetworkImageProvider(avatarImg), + onBackgroundImageError: + (exception, stackTrace) => + Image.asset( + "assets/user-placeholder.png", + height: 16, + width: 16, + ), ), - ), - ], + const SizedBox( + width: 10, + ), + Flexible( + child: Text( + data.displayName ?? "Guest", + maxLines: 1, + softWrap: false, + overflow: TextOverflow.fade, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), ), IconButton( icon: const Icon(Icons.settings_outlined), @@ -176,6 +194,12 @@ class Sidebar extends HookConsumerWidget { child: CircleAvatar( backgroundImage: CachedNetworkImageProvider(avatarImg), + onBackgroundImageError: (exception, stackTrace) => + Image.asset( + "assets/user-placeholder.png", + height: 16, + width: 16, + ), ), ), ); diff --git a/lib/components/Library/UserDownloads.dart b/lib/components/Library/UserDownloads.dart index ac7d3fd2..916c0c95 100644 --- a/lib/components/Library/UserDownloads.dart +++ b/lib/components/Library/UserDownloads.dart @@ -69,8 +69,8 @@ class UserDownloads extends HookConsumerWidget { ), horizontalTitleGap: 5, subtitle: Text( - TypeConversionUtils.artists_X_String( - track.artists ?? [], + TypeConversionUtils.artists_X_String( + track.artists ?? [], ), ), ); diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 760d34fc..1dd350cc 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -20,18 +20,18 @@ class UserLibrary extends ConsumerWidget { isScrollable: true, tabs: [ Tab(text: "Playlist"), - Tab(text: "Artists"), - Tab(text: "Album"), Tab(text: "Downloads"), Tab(text: "Local"), + Tab(text: "Artists"), + Tab(text: "Album"), ], ), body: TabBarView(children: [ const AnonymousFallback(child: UserPlaylists()), - AnonymousFallback(child: UserArtists()), - const AnonymousFallback(child: UserAlbums()), const UserDownloads(), const UserLocalTracks(), + AnonymousFallback(child: UserArtists()), + const AnonymousFallback(child: UserAlbums()), ]), ), ), diff --git a/lib/components/Library/UserLocalTracks.dart b/lib/components/Library/UserLocalTracks.dart index 2aa56cc9..8648467e 100644 --- a/lib/components/Library/UserLocalTracks.dart +++ b/lib/components/Library/UserLocalTracks.dart @@ -1,13 +1,26 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:dart_tags/dart_tags.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_media_metadata/flutter_media_metadata.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mime/mime.dart'; +import 'package:mp3_info/mp3_info.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart'; +import 'package:spotube/components/Shared/TrackTile.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; +import 'package:spotube/models/Logger.dart'; +import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:id3/id3.dart'; + +final tagProcessor = TagProcessor(); const supportedAudioTypes = [ "audio/webm", @@ -18,54 +31,193 @@ const supportedAudioTypes = [ "audio/aac", ]; -List usePullLocalTracks(WidgetRef ref) { +const imgMimeToExt = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/webp": ".webp", + "image/gif": ".gif", +}; + +final localTracksProvider = FutureProvider>((ref) async { final downloadDir = Directory( ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)), ); - final localTracks = useState>([]); + if (!await downloadDir.exists()) { + await downloadDir.create(recursive: true); + return []; + } + final entities = downloadDir.listSync(recursive: true); + final filesWithMetadata = (await Future.wait( + entities.map((e) => File(e.path)).where((file) { + final mimetype = lookupMimeType(file.path); + return mimetype != null && supportedAudioTypes.contains(mimetype); + }).map( + (f) async { + final bytes = f.readAsBytes(); + final mp3Instance = MP3Instance(await bytes); - useEffect(() { - (() async { - if (!await downloadDir.exists()) { - await downloadDir.create(recursive: true); - return; - } - final entities = downloadDir.listSync(recursive: true); - final filesWithMetadata = (await Future.wait( - entities.map((e) => File(e.path)).where((file) { - final mimetype = lookupMimeType(file.path); - return mimetype != null && supportedAudioTypes.contains(mimetype); - }).map( - (f) async => { - "metadata": await MetadataRetriever.fromFile(f), - "file": f, - }, + final imageFile = mp3Instance.parseTagsSync() + ? File(join( + (await getTemporaryDirectory()).path, + "spotube", + basenameWithoutExtension(f.path) + + imgMimeToExt[ + mp3Instance.metaTags["APIC"]?["mime"] ?? "image/jpeg"]!, + )) + : null; + if (imageFile != null && + !await imageFile.exists() && + mp3Instance.metaTags["APIC"]?["base64"] != null) { + await imageFile.create(recursive: true); + await imageFile.writeAsBytes( + base64Decode( + mp3Instance.metaTags["APIC"]["base64"], + ), + mode: FileMode.writeOnly, + ); + } + Duration duration; + try { + duration = MP3Processor.fromBytes(await bytes).duration; + } catch (e, stack) { + getLogger(MP3Processor).e("[Parsing Mp3]", e, stack); + duration = Duration.zero; + } + + final metadata = await tagProcessor.getTagsFromByteArray(bytes); + return { + "metadata": metadata, + "file": f, + "art": imageFile?.path, + "duration": duration, + }; + }, + ), + )); + + final tracks = filesWithMetadata + .map( + (fileWithMetadata) => TypeConversionUtils.localTrack_X_Track( + fileWithMetadata["metadata"] as List, + fileWithMetadata["file"] as File, + fileWithMetadata["duration"] as Duration, + fileWithMetadata["art"] as String?, ), - )); + ) + .toList(); - final tracks = filesWithMetadata - .map( - (fileWithMetadata) => TypeConversionUtils.localTrack_X_Track( - fileWithMetadata["metadata"] as Metadata, - fileWithMetadata["file"] as File), - ) - .toList(); - - localTracks.value = tracks; - })(); - - return; - }, [downloadDir]); - - return localTracks.value; -} + return tracks; +}); class UserLocalTracks extends HookConsumerWidget { const UserLocalTracks({Key? key}) : super(key: key); + void playLocalTracks(Playback playback, List tracks, + {Track? currentTrack}) async { + currentTrack ??= tracks.first; + final isPlaylistPlaying = playback.playlist?.id == "local"; + if (!isPlaylistPlaying) { + await playback.playPlaylist( + CurrentPlaylist( + tracks: tracks, + id: "local", + name: "Local Tracks", + thumbnail: TypeConversionUtils.image_X_UrlString(null), + isLocal: true, + ), + tracks.indexWhere((s) => s.id == currentTrack?.id), + ); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playback.track?.id) { + await playback.play(currentTrack); + } + } + @override Widget build(BuildContext context, ref) { - final tracks = usePullLocalTracks(ref); - return Column(); + final playback = ref.watch(playbackProvider); + final isPlaylistPlaying = playback.playlist?.id == "local"; + final trackSnapshot = ref.watch(localTracksProvider); + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const SizedBox(width: 10), + ElevatedButton.icon( + label: const Text("Play"), + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, + ), + onPressed: trackSnapshot.value != null + ? () { + if (trackSnapshot.value?.isNotEmpty == true) { + if (!isPlaylistPlaying) { + playLocalTracks(playback, trackSnapshot.value!); + } else { + playback.stop(); + } + } + } + : null, + ), + const Spacer(), + ElevatedButton( + child: const Icon(Icons.refresh_rounded), + onPressed: () { + ref.refresh(localTracksProvider); + }, + ) + ], + ), + ), + trackSnapshot.when( + data: (tracks) { + return Expanded( + child: ListView.builder( + itemCount: tracks.length, + itemBuilder: (context, index) { + final track = tracks[index]; + return TrackTile( + playback, + duration: + "${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}", + track: MapEntry(index, track), + isActive: playback.track?.id == track.id, + isChecked: false, + showCheck: false, + thumbnailUrl: track.album?.images?.isNotEmpty == true + ? track.album?.images?.single.url + : "assets/album-placeholder.png", + isLocal: true, + onTrackPlayButtonPressed: (currentTrack) { + if (tracks.isNotEmpty) { + if (!isPlaylistPlaying) { + playLocalTracks( + playback, + tracks, + currentTrack: track, + ); + } else { + playback.stop(); + } + } + }, + ); + }, + ), + ); + }, + loading: () => const ShimmerTrackTile(noSliver: true), + error: (error, stackTrace) => + Text(error.toString() + stackTrace.toString()), + ) + ], + ); + ; } } diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index b825c276..1610460e 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -11,6 +11,7 @@ import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart'; @@ -132,10 +133,7 @@ class SyncedLyrics extends HookConsumerWidget { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( image: DecorationImage( - image: CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - ), + image: UniversalImage.imageProvider(albumArt), fit: BoxFit.cover, ), ), diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 505ebd2d..3a0e0832 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -21,10 +21,12 @@ class Player extends HookConsumerWidget { final breakpoint = useBreakpoints(); String albumArt = useMemoized( - () => TypeConversionUtils.image_X_UrlString( - playback.track?.album?.images, - index: (playback.track?.album?.images?.length ?? 1) - 1, - ), + () => playback.track?.album?.images?.isNotEmpty == true + ? TypeConversionUtils.image_X_UrlString( + playback.track?.album?.images, + index: (playback.track?.album?.images?.length ?? 1) - 1, + ) + : "assets/album-placeholder.png", [playback.track?.album?.images], ); diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart index b66f6816..4a8a0342 100644 --- a/lib/components/Player/PlayerTrackDetails.dart +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -1,6 +1,6 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -21,16 +21,15 @@ class PlayerTrackDetails extends HookConsumerWidget { if (albumArt != null) Padding( padding: const EdgeInsets.all(5.0), - child: CachedNetworkImage( - imageUrl: albumArt!, - maxHeightDiskCache: 50, - maxWidthDiskCache: 50, - cacheKey: albumArt, + child: UniversalImage( + path: albumArt!, + height: 50, + width: 50, placeholder: (context, url) { - return Container( + return Image.asset( + "assets/album-placeholder.png", height: 50, width: 50, - color: Theme.of(context).primaryColor, ); }, ), diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index 0d90ae74..4a33c16d 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -10,6 +10,7 @@ import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; @@ -57,10 +58,7 @@ class PlayerView extends HookConsumerWidget { body: Container( decoration: BoxDecoration( image: DecorationImage( - image: CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - ), + image: UniversalImage.imageProvider(albumArt), fit: BoxFit.cover, ), ), @@ -121,10 +119,8 @@ class PlayerView extends HookConsumerWidget { shape: BoxShape.circle, ), child: CircleAvatar( - backgroundImage: CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - ), + backgroundImage: + UniversalImage.imageProvider(albumArt), radius: MediaQuery.of(context).size.width * (breakpoint.isSm ? 0.4 : 0.3), ), diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart index e768d840..64b395f4 100644 --- a/lib/components/Shared/DownloadTrackButton.dart +++ b/lib/components/Shared/DownloadTrackButton.dart @@ -4,6 +4,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'; +import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -89,6 +90,7 @@ class DownloadTrackButton extends HookConsumerWidget { }, onDone: () async { status.value = TrackStatus.done; + ref.refresh(localTracksProvider); await Future.delayed( const Duration(seconds: 3), () { @@ -187,7 +189,11 @@ class DownloadTrackButton extends HookConsumerWidget { icon: Icon( outputFileExists ? Icons.download_done_rounded : Icons.download_rounded, ), - onPressed: track != null && track is SpotubeTrack ? _downloadTrack : null, + onPressed: track != null && + track is SpotubeTrack && + playback.playlist?.isLocal != true + ? _downloadTrack + : null, ); } } diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index e0988905..c09c1d39 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -1,11 +1,11 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart' hide Action; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Image; import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart'; import 'package:spotube/components/Shared/LinkText.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/models/Logger.dart'; @@ -32,6 +32,8 @@ class TrackTile extends HookConsumerWidget { final bool isChecked; final bool showCheck; + + final bool isLocal; final void Function(bool?)? onCheckChange; TrackTile( @@ -46,12 +48,17 @@ class TrackTile extends HookConsumerWidget { this.showAlbum = true, this.isChecked = false, this.showCheck = false, + this.isLocal = false, this.onCheckChange, Key? key, }) : super(key: key); @override Widget build(BuildContext context, ref) { + final isReallyLocal = isLocal || + ref.watch( + playbackProvider.select((s) => s.playlist?.isLocal == true), + ); final breakpoint = useBreakpoints(); final auth = ref.watch(authProvider); final spotify = ref.watch(spotifyProvider); @@ -60,7 +67,7 @@ class TrackTile extends HookConsumerWidget { final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery); final isSaved = savedTracksSnapshot.asData?.value.any( - (e) => track.value.id! == e.id, + (e) => track.value.id == e.id, ) ?? false; @@ -210,17 +217,17 @@ class TrackTile extends HookConsumerWidget { ), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(5)), - child: CachedNetworkImage( + child: UniversalImage( + path: thumbnailUrl!, + height: 40, + width: 40, placeholder: (context, url) { - return Container( + return Image.asset( + "assets/album-placeholder.png", height: 40, width: 40, - color: Theme.of(context).primaryColor, ); }, - imageUrl: thumbnailUrl!, - maxHeightDiskCache: 40, - maxWidthDiskCache: 40, ), ), ), @@ -248,61 +255,70 @@ class TrackTile extends HookConsumerWidget { ), overflow: TextOverflow.ellipsis, ), - TypeConversionUtils.artists_X_ClickableArtists( - track.value.artists ?? [], - textStyle: TextStyle( - fontSize: - breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)), + isReallyLocal + ? Text( + TypeConversionUtils.artists_X_String( + track.value.artists ?? []), + ) + : TypeConversionUtils.artists_X_ClickableArtists( + track.value.artists ?? [], + textStyle: TextStyle( + fontSize: breakpoint.isLessThan(Breakpoints.lg) + ? 12 + : 14)), ], ), ), if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) Expanded( - child: LinkText( - track.value.album!.name!, - "/album/${track.value.album?.id}", - extra: track.value.album, - overflow: TextOverflow.ellipsis, - ), + child: isReallyLocal + ? Text(track.value.album?.name ?? "") + : LinkText( + track.value.album!.name!, + "/album/${track.value.album?.id}", + extra: track.value.album, + overflow: TextOverflow.ellipsis, + ), ), if (!breakpoint.isSm) ...[ const SizedBox(width: 10), Text(duration), ], const SizedBox(width: 10), - AdaptiveActions( - actions: [ - if (auth.isLoggedIn) + if (!isReallyLocal) + AdaptiveActions( + actions: [ + if (auth.isLoggedIn) + Action( + icon: Icon(isSaved + ? Icons.favorite_rounded + : Icons.favorite_border_rounded), + text: const Text("Save as favorite"), + onPressed: () { + actionFavorite(isSaved); + }, + ), + if (auth.isLoggedIn) + Action( + icon: const Icon(Icons.add_box_rounded), + text: const Text("Add To playlist"), + onPressed: actionAddToPlaylist, + ), + if (userPlaylist && auth.isLoggedIn) + Action( + icon: const Icon(Icons.remove_circle_outline_rounded), + text: const Text("Remove from playlist"), + onPressed: actionRemoveFromPlaylist, + ), Action( - icon: Icon(isSaved - ? Icons.favorite_rounded - : Icons.favorite_border_rounded), - text: const Text("Save as favorite"), + icon: const Icon(Icons.share_rounded), + text: const Text("Share"), onPressed: () { - actionFavorite(isSaved); + actionShare(track.value); }, - ), - if (auth.isLoggedIn) - Action( - icon: const Icon(Icons.add_box_rounded), - text: const Text("Add To playlist"), - onPressed: actionAddToPlaylist, - ), - if (userPlaylist && auth.isLoggedIn) - Action( - icon: const Icon(Icons.remove_circle_outline_rounded), - text: const Text("Remove from playlist"), - onPressed: actionRemoveFromPlaylist, - ), - Action( - icon: const Icon(Icons.share_rounded), - text: const Text("Share"), - onPressed: () { - actionShare(track.value); - }, - ) - ], - ), + ) + ], + ), ], ), ), diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index d38f6981..4edcbd88 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -17,6 +17,7 @@ class TracksTableView extends HookConsumerWidget { final bool userPlaylist; final String? playlistId; final bool bottomSpace; + final bool isSliver; final Widget? heading; const TracksTableView( @@ -27,6 +28,7 @@ class TracksTableView extends HookConsumerWidget { this.playlistId, this.heading, this.bottomSpace = false, + this.isSliver = true, }) : super(key: key); @override @@ -48,156 +50,153 @@ class TracksTableView extends HookConsumerWidget { [tracks], ); - return SliverList( - delegate: SliverChildListDelegate( - [ - if (heading != null) heading!, - Row( - children: [ - Checkbox( - value: selected.value.length == tracks.length, - onChanged: (checked) { - if (!showCheck.value) showCheck.value = true; - if (checked == true) { - selected.value = tracks.map((s) => s.id!).toList(); - } else { - selected.value = []; - showCheck.value = false; - } - }, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "#", - textAlign: TextAlign.center, + final children = [ + if (heading != null) heading!, + Row( + children: [ + Checkbox( + value: selected.value.length == tracks.length, + onChanged: (checked) { + if (!showCheck.value) showCheck.value = true; + if (checked == true) { + selected.value = tracks.map((s) => s.id!).toList(); + } else { + selected.value = []; + showCheck.value = false; + } + }, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "#", + textAlign: TextAlign.center, + style: tableHeadStyle, + ), + ), + Expanded( + child: Row( + children: [ + Text( + "Title", style: tableHeadStyle, + overflow: TextOverflow.ellipsis, ), + ], + ), + ), + // used alignment of this table-head + if (breakpoint.isMoreThan(Breakpoints.md)) ...[ + const SizedBox(width: 100), + Expanded( + child: Row( + children: [ + Text( + "Album", + overflow: TextOverflow.ellipsis, + style: tableHeadStyle, + ), + ], ), - Expanded( - child: Row( - children: [ - Text( - "Title", - style: tableHeadStyle, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - // used alignment of this table-head - if (breakpoint.isMoreThan(Breakpoints.md)) ...[ - const SizedBox(width: 100), - Expanded( + ) + ], + if (!breakpoint.isSm) ...[ + const SizedBox(width: 10), + Text("Time", style: tableHeadStyle), + const SizedBox(width: 10), + ], + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + enabled: selected.value.isNotEmpty, child: Row( children: [ + const Icon(Icons.file_download_outlined), Text( - "Album", - overflow: TextOverflow.ellipsis, - style: tableHeadStyle, + "Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}", ), ], ), - ) - ], - if (!breakpoint.isSm) ...[ - const SizedBox(width: 10), - Text("Time", style: tableHeadStyle), - const SizedBox(width: 10), - ], - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( - enabled: selected.value.isNotEmpty, - child: Row( - children: [ - const Icon(Icons.file_download_outlined), - Text( - "Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}", - ), - ], - ), - value: "download", - ), - ]; - }, - onSelected: (action) async { - switch (action) { - case "download": - { - final isConfirmed = await showDialog( - context: context, - builder: (context) { - return const DownloadConfirmationDialog(); - }); - if (isConfirmed != true) return; - for (final selectedTrack in selectedTracks) { - downloader.addToQueue(selectedTrack); - } - selected.value = []; - showCheck.value = false; - break; - } - default: + value: "download", + ), + ]; + }, + onSelected: (action) async { + switch (action) { + case "download": + { + final isConfirmed = await showDialog( + context: context, + builder: (context) { + return const DownloadConfirmationDialog(); + }); + if (isConfirmed != true) return; + for (final selectedTrack in selectedTracks) { + downloader.addToQueue(selectedTrack); + } + selected.value = []; + showCheck.value = false; + break; } - }, - ), - ], + default: + } + }, ), - ...tracks.asMap().entries.map((track) { - String? thumbnailUrl = TypeConversionUtils.image_X_UrlString( - track.value.album?.images, - index: (track.value.album?.images?.length ?? 1) - 1, - ); - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return InkWell( - onLongPress: () { - showCheck.value = true; - selected.value = [...selected.value, track.value.id!]; - }, - onTap: () { - if (showCheck.value) { - final alreadyChecked = - selected.value.contains(track.value.id); - if (alreadyChecked) { - selected.value = selected.value - .where((id) => id != track.value.id) - .toList(); - } else { - selected.value = [...selected.value, track.value.id!]; - } - } else { - onTrackPlayButtonPressed?.call(track.value); - } - }, - child: TrackTile( - playback, - playlistId: playlistId, - track: track, - duration: duration, - thumbnailUrl: thumbnailUrl, - userPlaylist: userPlaylist, - isActive: playback.track?.id == track.value.id, - onTrackPlayButtonPressed: onTrackPlayButtonPressed, - isChecked: selected.value.contains(track.value.id), - showCheck: showCheck.value, - onCheckChange: (checked) { - if (checked == true) { - selected.value = [...selected.value, track.value.id!]; - } else { - selected.value = selected.value - .where((id) => id != track.value.id) - .toList(); - } - }, - ), - ); - }).toList(), - if (bottomSpace) const SizedBox(height: 70), ], ), - ); + ...tracks.asMap().entries.map((track) { + String? thumbnailUrl = TypeConversionUtils.image_X_UrlString( + track.value.album?.images, + index: (track.value.album?.images?.length ?? 1) - 1, + ); + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return InkWell( + onLongPress: () { + showCheck.value = true; + selected.value = [...selected.value, track.value.id!]; + }, + onTap: () { + if (showCheck.value) { + final alreadyChecked = selected.value.contains(track.value.id); + if (alreadyChecked) { + selected.value = + selected.value.where((id) => id != track.value.id).toList(); + } else { + selected.value = [...selected.value, track.value.id!]; + } + } else { + onTrackPlayButtonPressed?.call(track.value); + } + }, + child: TrackTile( + playback, + playlistId: playlistId, + track: track, + duration: duration, + thumbnailUrl: thumbnailUrl, + userPlaylist: userPlaylist, + isActive: playback.track?.id == track.value.id, + onTrackPlayButtonPressed: onTrackPlayButtonPressed, + isChecked: selected.value.contains(track.value.id), + showCheck: showCheck.value, + onCheckChange: (checked) { + if (checked == true) { + selected.value = [...selected.value, track.value.id!]; + } else { + selected.value = + selected.value.where((id) => id != track.value.id).toList(); + } + }, + ), + ); + }).toList(), + if (bottomSpace) const SizedBox(height: 70), + ]; + if (isSliver) { + return SliverList(delegate: SliverChildListDelegate(children)); + } + return ListView(children: children); } } diff --git a/lib/components/Shared/UniversalImage.dart b/lib/components/Shared/UniversalImage.dart new file mode 100644 index 00000000..6a4aa373 --- /dev/null +++ b/lib/components/Shared/UniversalImage.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class UniversalImage extends HookWidget { + final String path; + final double? height; + final double? width; + final double scale; + final PlaceholderWidgetBuilder? placeholder; + const UniversalImage({ + required this.path, + this.height, + this.width, + this.placeholder, + this.scale = 1, + Key? key, + }) : super(key: key); + + static ImageProvider imageProvider( + String path, { + final double? height, + final double? width, + final double scale = 1, + }) { + if (path.startsWith("http")) { + return CachedNetworkImageProvider( + path, + maxHeight: height?.toInt(), + maxWidth: width?.toInt(), + cacheKey: path, + scale: scale, + ); + } else if (Uri.tryParse(path) != null) { + return FileImage(File(path), scale: scale); + } + return MemoryImage(base64Decode(path), scale: scale); + } + + @override + Widget build(BuildContext context) { + if (path.startsWith("http")) { + return CachedNetworkImage( + imageUrl: path, + height: height, + width: width, + maxWidthDiskCache: width?.toInt(), + maxHeightDiskCache: height?.toInt(), + memCacheHeight: height?.toInt(), + memCacheWidth: width?.toInt(), + placeholder: placeholder, + cacheKey: path, + ); + } else if (Uri.tryParse(path) != null) { + return Image.file( + File(path), + width: width, + height: height, + cacheHeight: height?.toInt(), + cacheWidth: width?.toInt(), + scale: scale, + errorBuilder: (context, error, stackTrace) { + return placeholder?.call(context, error.toString()) ?? + Image.asset( + "assets/placeholder.png", + width: width, + height: height, + cacheHeight: height?.toInt(), + cacheWidth: width?.toInt(), + scale: scale, + ); + }, + ); + } + return Image.memory( + base64Decode(path), + width: width, + height: height, + cacheHeight: height?.toInt(), + cacheWidth: width?.toInt(), + scale: scale, + errorBuilder: (context, error, stackTrace) { + return placeholder?.call(context, error.toString()) ?? + Image.asset( + "assets/placeholder.png", + width: width, + height: height, + cacheHeight: height?.toInt(), + cacheWidth: width?.toInt(), + scale: scale, + ); + }, + ); + } +} diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart index e5a472f5..9809fb26 100644 --- a/lib/hooks/usePaletteColor.dart +++ b/lib/hooks/usePaletteColor.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; final _paletteColorState = StateProvider( (ref) { @@ -18,11 +19,10 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final palette = await PaletteGenerator.fromImageProvider( - CachedNetworkImageProvider( + UniversalImage.imageProvider( imageUrl, - cacheKey: imageUrl, - maxHeight: 50, - maxWidth: 50, + height: 50, + width: 50, ), ); if (!mounted()) return; @@ -49,11 +49,10 @@ PaletteGenerator usePaletteGenerator( useEffect(() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final newPalette = await PaletteGenerator.fromImageProvider( - CachedNetworkImageProvider( + UniversalImage.imageProvider( imageUrl, - cacheKey: imageUrl, - maxHeight: 50, - maxWidth: 50, + height: 50, + width: 50, ), ); if (!mounted()) return; diff --git a/lib/models/CurrentPlaylist.dart b/lib/models/CurrentPlaylist.dart index 75322941..cacb42ff 100644 --- a/lib/models/CurrentPlaylist.dart +++ b/lib/models/CurrentPlaylist.dart @@ -61,12 +61,14 @@ class CurrentPlaylist { String id; String name; String thumbnail; + bool isLocal; CurrentPlaylist({ required this.tracks, required this.id, required this.name, required this.thumbnail, + this.isLocal = false, }); static CurrentPlaylist fromJson(Map map) { @@ -76,6 +78,7 @@ class CurrentPlaylist { map["tracks"].map((track) => Track.fromJson(track)).toList()), name: map["name"], thumbnail: map["thumbnail"], + isLocal: map["isLocal"], ); } @@ -107,6 +110,7 @@ class CurrentPlaylist { "name": name, "tracks": tracks.map((track) => track.toJson()).toList(), "thumbnail": thumbnail, + "isLocal": isLocal, }; } } diff --git a/lib/models/Id3Tags.dart b/lib/models/Id3Tags.dart new file mode 100644 index 00000000..746773ab --- /dev/null +++ b/lib/models/Id3Tags.dart @@ -0,0 +1,143 @@ +import 'package:dart_tags/dart_tags.dart'; + +class Id3Tags { + Id3Tags({ + this.tsse, + this.title, + this.album, + this.tpe2, + this.comment, + this.tcop, + this.tdrc, + this.genre, + this.picture, + }); + + String? tsse; + String? title; + String? album; + String? tpe2; + Comment? comment; + String? tcop; + String? tdrc; + String? genre; + AttachedPicture? picture; + + factory Id3Tags.fromJson(Map json) => Id3Tags( + tsse: json["TSSE"], + title: json["title"], + album: json["album"], + tpe2: json["TPE2"], + comment: json["comment"]?["eng:"] is Comment + ? json["comment"]["eng:"] + : CommentJson.fromJson(Map.from( + json["comment"]?["eng:"] ?? {}, + )), + tcop: json["TCOP"], + tdrc: json["TDRC"], + genre: json["genre"], + picture: json["picture"]?["Cover (front)"] is AttachedPicture + ? json["picture"]["Cover (front)"] + : AttachedPictureJson.fromJson(Map.from( + json["picture"]?["Cover (front)"] ?? {}, + )), + ); + + factory Id3Tags.fromId3v1Tags(Id3v1Tags v1tags) => Id3Tags( + album: v1tags.album, + comment: Comment("", "", v1tags.comment ?? ""), + genre: v1tags.genre, + title: v1tags.title, + tcop: v1tags.year, + tdrc: v1tags.year, + tpe2: v1tags.artist, + ); + + Map toJson() => { + "TSSE": tsse, + "title": title, + "album": album, + "TPE2": tpe2, + "comment": comment, + "TCOP": tcop, + "TDRC": tdrc, + "genre": genre, + "picture": picture, + }; +} + +extension CommentJson on Comment { + static fromJson(Map json) => Comment( + json["lang"] ?? "", + json["description"] ?? "", + json["comment"] ?? "", + ); + + Map toJson() => { + "comment": comment, + "description": description, + "key": key, + "lang": lang, + }; +} + +extension AttachedPictureJson on AttachedPicture { + static fromJson(Map json) => AttachedPicture( + json["mime"] ?? "", + json["imageTypeCode"] ?? 0, + json["description"] ?? "", + List.from(json["imageData"] ?? []), + ); + + Map toJson() => { + "description": description, + "imageData": imageData, + "imageData64": imageData64, + "imageType": imageType, + "imageTypeCode": imageTypeCode, + "key": key, + "mime": mime, + }; +} + +class Id3v1Tags { + String? title; + String? artist; + String? album; + String? year; + String? comment; + String? track; + String? genre; + + Id3v1Tags({ + this.title, + this.artist, + this.album, + this.year, + this.comment, + this.track, + this.genre, + }); + + Id3v1Tags.fromJson(Map json) { + title = json['title']; + artist = json['artist']; + album = json['album']; + year = json['year']; + comment = json['comment']; + track = json['track']; + genre = json['genre']; + } + + Map toJson() { + return { + 'title': title, + 'artist': artist, + 'album': album, + 'year': year, + 'comment': comment, + 'track': track, + 'genre': genre, + }; + } +} diff --git a/lib/provider/Downloader.dart b/lib/provider/Downloader.dart index 75c5f4cc..68634e31 100644 --- a/lib/provider/Downloader.dart +++ b/lib/provider/Downloader.dart @@ -1,17 +1,23 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'package:dart_tags/dart_tags.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; import 'package:queue/queue.dart'; import 'package:path/path.dart' as path; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Library/UserLocalTracks.dart'; +import 'package:spotube/models/Id3Tags.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/YouTube.dart'; -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment; Queue queueInstance = Queue(delay: const Duration(seconds: 5)); Queue grabberQueue = Queue(delay: const Duration(seconds: 5)); @@ -89,6 +95,54 @@ class Downloader with ChangeNotifier { logger.v( "[addToQueue] Download of ${file.path} is done successfully", ); + + final response = await get( + Uri.parse( + TypeConversionUtils.image_X_UrlString( + track.album?.images ?? [], + ), + ), + ); + final picture = AttachedPicture.base64( + response.headers["Content-Type"] ?? "image/jpeg", + 3, + track.name!, + base64Encode(response.bodyBytes), + ); + // write id3 metadata + final tag = Id3Tags( + album: track.album?.name, + picture: picture, + title: track.name, + genre: "Spotube", + tcop: track.ytTrack.uploadDate?.year.toString(), + tdrc: track.ytTrack.uploadDate?.year.toString(), + tpe2: TypeConversionUtils.artists_X_String( + track.artists ?? [], + ), + tsse: "", + comment: Comment( + "eng", + track.ytTrack.description, + track.ytTrack.title, + ), + ); + + logger.v("[addToQueue] Writing metadata to ${file.path}"); + + final taggedMp3 = await tagProcessor.putTagsToByteArray( + file.readAsBytes(), + [ + Tag() + ..type = "ID3" + ..version = "2.4.0" + ..tags = tag.toJson() + ], + ); + await file.writeAsBytes(taggedMp3); + logger.v( + "[addToQueue] Writing metadata to ${file.path} is successful", + ); } catch (e, stack) { logger.e( "[addToQueue] Failed download of ${file.path}", diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 353774d4..f1cca746 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -208,7 +208,10 @@ class Playback extends PersistedChangeNotifier { artist: TypeConversionUtils.artists_X_String( track.artists ?? []), artUri: Uri.parse( - TypeConversionUtils.image_X_UrlString(track.album?.images)), + TypeConversionUtils.image_X_UrlString( + track.album?.images, + ), + ), duration: track.ytTrack.duration, ); mobileAudioService?.addItem(tag); @@ -216,7 +219,11 @@ class Playback extends PersistedChangeNotifier { this.track = track; notifyListeners(); updatePersistence(); - await player.play(UrlSource(track.ytUri)); + await player.play( + track.ytUri.startsWith("http") + ? UrlSource(track.ytUri) + : DeviceFileSource(track.ytUri), + ); status = PlaybackStatus.playing; notifyListeners(); } catch (e, stack) { diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index 7d617da9..d887637a 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -2,11 +2,16 @@ import 'dart:io'; +import 'package:dart_tags/dart_tags.dart'; import 'package:flutter/widgets.dart' hide Image; -import 'package:flutter_media_metadata/flutter_media_metadata.dart'; +import 'package:path/path.dart'; import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/Id3Tags.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/utils/primitive_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; abstract class TypeConversionUtils { static String image_X_UrlString(List? images, {int index = 0}) { @@ -85,31 +90,61 @@ abstract class TypeConversionUtils { return track; } - static Track localTrack_X_Track(Metadata metadata, File file) { - final track = Track(); + static SpotubeTrack localTrack_X_Track( + List metadatas, + File file, + Duration duration, + String? art, + ) { + final v2Tags = + metadatas.firstWhereOrNull((s) => s.version == "2.4.0")?.tags; + final v1Tags = + metadatas.firstWhereOrNull((s) => s.version != "2.4.0")?.tags; + final metadata = v2Tags != null + ? Id3Tags.fromJson(v2Tags) + : Id3Tags.fromId3v1Tags(Id3v1Tags.fromJson(v1Tags ?? {})); + final track = SpotubeTrack( + Video( + VideoId("dQw4w9WgXcQ"), + basenameWithoutExtension(file.path), + metadata.tpe2 ?? "", + ChannelId( + "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + ), + DateTime.now(), + DateTime.now(), + "", + duration, + ThumbnailSet(metadata.title ?? ""), + [], + const Engagement(0, 0, 0), + false, + ), + file.path, + [], + ); track.album = Album() - ..name = metadata.albumName + ..name = metadata.album ?? "Spotube" + ..images = [if (art != null) Image()..url = art] ..genres = [if (metadata.genre != null) metadata.genre!] ..artists = [ Artist() - ..name = metadata.albumArtistName - ..id = metadata.albumArtistName + ..name = metadata.tpe2 ?? "Spotube" + ..id = metadata.tpe2 ?? "Spotube" ..type = "artist", ] - ..id = "${metadata.albumName}${metadata.albumLength}"; - track.artists = metadata.trackArtistNames - ?.map((name) => Artist() - ..name = name - ..id = name) - .toList(); + ..id = metadata.album; + track.artists = [ + Artist() + ..name = metadata.tpe2 ?? "Spotube" + ..id = metadata.tpe2 ?? "Spotube" + ]; - track.discNumber = metadata.discNumber; - track.durationMs = metadata.trackDuration; - track.id = "${metadata.trackName}${metadata.trackDuration}"; - track.name = metadata.trackName; - track.trackNumber = metadata.trackNumber; + track.id = metadata.title ?? basenameWithoutExtension(file.path); + track.name = metadata.title ?? basenameWithoutExtension(file.path); track.type = "track"; track.uri = file.path; + track.durationMs = duration.inMilliseconds; return track; } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index cce5b687..01b8e0f7 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ #include #include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -18,9 +17,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); - g_autoptr(FlPluginRegistrar) flutter_media_metadata_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterMediaMetadataPlugin"); - flutter_media_metadata_plugin_register_with_registrar(flutter_media_metadata_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4e314d80..9aebc645 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,7 +5,6 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux bitsdojo_window_linux - flutter_media_metadata url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 594b241f..3e375fb3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import audio_service import audio_session import audioplayers_darwin import bitsdojo_window_macos -import flutter_media_metadata import package_info_plus_macos import path_provider_macos import shared_preferences_macos @@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) - FlutterMediaMetadataPlugin.register(with: registry.registrar(forPlugin: "FlutterMediaMetadataPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ce4760d1..9922b11b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -140,7 +140,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" audio_service: dependency: "direct main" description: @@ -357,14 +357,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: @@ -378,7 +371,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -442,6 +435,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + dart_tags: + dependency: "direct main" + description: + name: dart_tags + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" dbus: dependency: "direct main" description: @@ -463,6 +463,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + eztags: + dependency: "direct main" + description: + name: eztags + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" fading_edge_scrollview: dependency: transitive description: @@ -476,7 +483,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -573,13 +580,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" - flutter_media_metadata: - dependency: "direct main" - description: - path: "../flutter_media_metadata" - relative: true - source: path - version: "1.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -702,6 +702,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + id3: + dependency: "direct main" + description: + name: id3 + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" image: dependency: transitive description: @@ -778,21 +785,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: "direct main" description: @@ -800,6 +807,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + mp3_info: + dependency: "direct main" + description: + name: mp3_info + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" msix: dependency: "direct dev" description: @@ -890,7 +904,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -1175,7 +1189,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" spotify: dependency: "direct main" description: @@ -1233,7 +1247,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1247,14 +1261,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -1325,6 +1339,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + utf_convert: + dependency: transitive + description: + name: utf_convert + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0+1" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d6b2bc1..9ea0fc31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,8 +71,9 @@ dependencies: auto_size_text: ^3.0.0 badges: ^2.0.3 mime: ^1.0.2 - flutter_media_metadata: - path: ../flutter_media_metadata + dart_tags: ^0.4.0 + id3: ^1.0.2 + mp3_info: ^0.2.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c8153a66..3e689c38 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -17,8 +16,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); - FlutterMediaMetadataPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterMediaMetadataPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d8bb9c42..c8e970a8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,6 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows bitsdojo_window_windows - flutter_media_metadata permission_handler_windows url_launcher_windows )