Merge branch 'KRTirtho:master' into feature_duration_matching

This commit is contained in:
Demizo 2022-09-02 23:23:12 -05:00 committed by GitHub
commit 7ae453b81d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 881 additions and 338 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
assets/user-placeholder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -148,21 +148,39 @@ class Sidebar extends HookConsumerWidget {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
else if (data != null) else if (data != null)
Row( Flexible(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundImage: backgroundImage:
CachedNetworkImageProvider(avatarImg), CachedNetworkImageProvider(avatarImg),
onBackgroundImageError:
(exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
), ),
const SizedBox(width: 10), ),
Text( const SizedBox(
width: 10,
),
Flexible(
child: Text(
data.displayName ?? "Guest", data.displayName ?? "Guest",
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
),
], ],
), ),
),
IconButton( IconButton(
icon: const Icon(Icons.settings_outlined), icon: const Icon(Icons.settings_outlined),
onPressed: () => goToSettings(context)), onPressed: () => goToSettings(context)),
@ -176,6 +194,12 @@ class Sidebar extends HookConsumerWidget {
child: CircleAvatar( child: CircleAvatar(
backgroundImage: backgroundImage:
CachedNetworkImageProvider(avatarImg), CachedNetworkImageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
),
), ),
), ),
); );

View File

@ -69,8 +69,8 @@ class UserDownloads extends HookConsumerWidget {
), ),
horizontalTitleGap: 5, horizontalTitleGap: 5,
subtitle: Text( subtitle: Text(
TypeConversionUtils.artists_X_String<Artist>( TypeConversionUtils.artists_X_String(
track.artists ?? [], track.artists ?? <Artist>[],
), ),
), ),
); );

View File

@ -20,18 +20,18 @@ class UserLibrary extends ConsumerWidget {
isScrollable: true, isScrollable: true,
tabs: [ tabs: [
Tab(text: "Playlist"), Tab(text: "Playlist"),
Tab(text: "Artists"),
Tab(text: "Album"),
Tab(text: "Downloads"), Tab(text: "Downloads"),
Tab(text: "Local"), Tab(text: "Local"),
Tab(text: "Artists"),
Tab(text: "Album"),
], ],
), ),
body: TabBarView(children: [ body: TabBarView(children: [
const AnonymousFallback(child: UserPlaylists()), const AnonymousFallback(child: UserPlaylists()),
AnonymousFallback(child: UserArtists()),
const AnonymousFallback(child: UserAlbums()),
const UserDownloads(), const UserDownloads(),
const UserLocalTracks(), const UserLocalTracks(),
AnonymousFallback(child: UserArtists()),
const AnonymousFallback(child: UserAlbums()),
]), ]),
), ),
), ),

View File

@ -1,13 +1,26 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:mime/mime.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: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/provider/UserPreferences.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:id3/id3.dart';
final tagProcessor = TagProcessor();
const supportedAudioTypes = [ const supportedAudioTypes = [
"audio/webm", "audio/webm",
@ -18,17 +31,20 @@ const supportedAudioTypes = [
"audio/aac", "audio/aac",
]; ];
List<Track> usePullLocalTracks(WidgetRef ref) { const imgMimeToExt = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/webp": ".webp",
"image/gif": ".gif",
};
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
final downloadDir = Directory( final downloadDir = Directory(
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)), ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)),
); );
final localTracks = useState<List<Track>>([]);
useEffect(() {
(() async {
if (!await downloadDir.exists()) { if (!await downloadDir.exists()) {
await downloadDir.create(recursive: true); await downloadDir.create(recursive: true);
return; return [];
} }
final entities = downloadDir.listSync(recursive: true); final entities = downloadDir.listSync(recursive: true);
final filesWithMetadata = (await Future.wait( final filesWithMetadata = (await Future.wait(
@ -36,9 +52,45 @@ List<Track> usePullLocalTracks(WidgetRef ref) {
final mimetype = lookupMimeType(file.path); final mimetype = lookupMimeType(file.path);
return mimetype != null && supportedAudioTypes.contains(mimetype); return mimetype != null && supportedAudioTypes.contains(mimetype);
}).map( }).map(
(f) async => { (f) async {
"metadata": await MetadataRetriever.fromFile(f), final bytes = f.readAsBytes();
final mp3Instance = MP3Instance(await bytes);
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, "file": f,
"art": imageFile?.path,
"duration": duration,
};
}, },
), ),
)); ));
@ -46,26 +98,126 @@ List<Track> usePullLocalTracks(WidgetRef ref) {
final tracks = filesWithMetadata final tracks = filesWithMetadata
.map( .map(
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track( (fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
fileWithMetadata["metadata"] as Metadata, fileWithMetadata["metadata"] as List<Tag>,
fileWithMetadata["file"] as File), fileWithMetadata["file"] as File,
fileWithMetadata["duration"] as Duration,
fileWithMetadata["art"] as String?,
),
) )
.toList(); .toList();
localTracks.value = tracks; return tracks;
})(); });
return;
}, [downloadDir]);
return localTracks.value;
}
class UserLocalTracks extends HookConsumerWidget { class UserLocalTracks extends HookConsumerWidget {
const UserLocalTracks({Key? key}) : super(key: key); const UserLocalTracks({Key? key}) : super(key: key);
void playLocalTracks(Playback playback, List<Track> 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 @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final tracks = usePullLocalTracks(ref); final playback = ref.watch(playbackProvider);
return Column(); 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()),
)
],
);
;
} }
} }

View File

@ -11,6 +11,7 @@ import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.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/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart';
@ -132,10 +133,7 @@ class SyncedLyrics extends HookConsumerWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: CachedNetworkImageProvider( image: UniversalImage.imageProvider(albumArt),
albumArt,
cacheKey: albumArt,
),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),

View File

@ -21,10 +21,12 @@ class Player extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
String albumArt = useMemoized( String albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString( () => playback.track?.album?.images?.isNotEmpty == true
? TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images, playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1, index: (playback.track?.album?.images?.length ?? 1) - 1,
), )
: "assets/album-placeholder.png",
[playback.track?.album?.images], [playback.track?.album?.images],
); );

View File

@ -1,6 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -21,16 +21,15 @@ class PlayerTrackDetails extends HookConsumerWidget {
if (albumArt != null) if (albumArt != null)
Padding( Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: CachedNetworkImage( child: UniversalImage(
imageUrl: albumArt!, path: albumArt!,
maxHeightDiskCache: 50, height: 50,
maxWidthDiskCache: 50, width: 50,
cacheKey: albumArt, placeholder: (context, url) {
placeholder: (context, url) { return Image.asset(
return Container( "assets/album-placeholder.png",
height: 50, height: 50,
width: 50, width: 50,
color: Theme.of(context).primaryColor,
); );
}, },
), ),

View File

@ -10,6 +10,7 @@ import 'package:spotube/components/Player/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.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/useBreakpoints.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
@ -57,10 +58,7 @@ class PlayerView extends HookConsumerWidget {
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: CachedNetworkImageProvider( image: UniversalImage.imageProvider(albumArt),
albumArt,
cacheKey: albumArt,
),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
@ -121,10 +119,8 @@ class PlayerView extends HookConsumerWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: CircleAvatar( child: CircleAvatar(
backgroundImage: CachedNetworkImageProvider( backgroundImage:
albumArt, UniversalImage.imageProvider(albumArt),
cacheKey: albumArt,
),
radius: MediaQuery.of(context).size.width * radius: MediaQuery.of(context).size.width *
(breakpoint.isSm ? 0.4 : 0.3), (breakpoint.isSm ? 0.4 : 0.3),
), ),

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -89,6 +90,7 @@ class DownloadTrackButton extends HookConsumerWidget {
}, },
onDone: () async { onDone: () async {
status.value = TrackStatus.done; status.value = TrackStatus.done;
ref.refresh(localTracksProvider);
await Future.delayed( await Future.delayed(
const Duration(seconds: 3), const Duration(seconds: 3),
() { () {
@ -187,7 +189,11 @@ class DownloadTrackButton extends HookConsumerWidget {
icon: Icon( icon: Icon(
outputFileExists ? Icons.download_done_rounded : Icons.download_rounded, 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,
); );
} }
} }

View File

@ -1,11 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart' hide Action; import 'package:flutter/material.dart' hide Action;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/AdaptivePopupMenuButton.dart';
import 'package:spotube/components/Shared/LinkText.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/useBreakpoints.dart';
import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
@ -32,6 +32,8 @@ class TrackTile extends HookConsumerWidget {
final bool isChecked; final bool isChecked;
final bool showCheck; final bool showCheck;
final bool isLocal;
final void Function(bool?)? onCheckChange; final void Function(bool?)? onCheckChange;
TrackTile( TrackTile(
@ -46,12 +48,17 @@ class TrackTile extends HookConsumerWidget {
this.showAlbum = true, this.showAlbum = true,
this.isChecked = false, this.isChecked = false,
this.showCheck = false, this.showCheck = false,
this.isLocal = false,
this.onCheckChange, this.onCheckChange,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final isReallyLocal = isLocal ||
ref.watch(
playbackProvider.select((s) => s.playlist?.isLocal == true),
);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
@ -60,7 +67,7 @@ class TrackTile extends HookConsumerWidget {
final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery); final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery);
final isSaved = savedTracksSnapshot.asData?.value.any( final isSaved = savedTracksSnapshot.asData?.value.any(
(e) => track.value.id! == e.id, (e) => track.value.id == e.id,
) ?? ) ??
false; false;
@ -210,17 +217,17 @@ class TrackTile extends HookConsumerWidget {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5)), borderRadius: const BorderRadius.all(Radius.circular(5)),
child: CachedNetworkImage( child: UniversalImage(
placeholder: (context, url) { path: thumbnailUrl!,
return Container( height: 40,
width: 40,
placeholder: (context, url) {
return Image.asset(
"assets/album-placeholder.png",
height: 40, height: 40,
width: 40, width: 40,
color: Theme.of(context).primaryColor,
); );
}, },
imageUrl: thumbnailUrl!,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
), ),
), ),
), ),
@ -248,17 +255,25 @@ class TrackTile extends HookConsumerWidget {
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
TypeConversionUtils.artists_X_ClickableArtists( isReallyLocal
? Text(
TypeConversionUtils.artists_X_String<Artist>(
track.value.artists ?? []),
)
: TypeConversionUtils.artists_X_ClickableArtists(
track.value.artists ?? [], track.value.artists ?? [],
textStyle: TextStyle( textStyle: TextStyle(
fontSize: fontSize: breakpoint.isLessThan(Breakpoints.lg)
breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)), ? 12
: 14)),
], ],
), ),
), ),
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
Expanded( Expanded(
child: LinkText( child: isReallyLocal
? Text(track.value.album?.name ?? "")
: LinkText(
track.value.album!.name!, track.value.album!.name!,
"/album/${track.value.album?.id}", "/album/${track.value.album?.id}",
extra: track.value.album, extra: track.value.album,
@ -270,6 +285,7 @@ class TrackTile extends HookConsumerWidget {
Text(duration), Text(duration),
], ],
const SizedBox(width: 10), const SizedBox(width: 10),
if (!isReallyLocal)
AdaptiveActions( AdaptiveActions(
actions: [ actions: [
if (auth.isLoggedIn) if (auth.isLoggedIn)

View File

@ -17,6 +17,7 @@ class TracksTableView extends HookConsumerWidget {
final bool userPlaylist; final bool userPlaylist;
final String? playlistId; final String? playlistId;
final bool bottomSpace; final bool bottomSpace;
final bool isSliver;
final Widget? heading; final Widget? heading;
const TracksTableView( const TracksTableView(
@ -27,6 +28,7 @@ class TracksTableView extends HookConsumerWidget {
this.playlistId, this.playlistId,
this.heading, this.heading,
this.bottomSpace = false, this.bottomSpace = false,
this.isSliver = true,
}) : super(key: key); }) : super(key: key);
@override @override
@ -48,9 +50,7 @@ class TracksTableView extends HookConsumerWidget {
[tracks], [tracks],
); );
return SliverList( final children = [
delegate: SliverChildListDelegate(
[
if (heading != null) heading!, if (heading != null) heading!,
Row( Row(
children: [ children: [
@ -159,12 +159,10 @@ class TracksTableView extends HookConsumerWidget {
}, },
onTap: () { onTap: () {
if (showCheck.value) { if (showCheck.value) {
final alreadyChecked = final alreadyChecked = selected.value.contains(track.value.id);
selected.value.contains(track.value.id);
if (alreadyChecked) { if (alreadyChecked) {
selected.value = selected.value selected.value =
.where((id) => id != track.value.id) selected.value.where((id) => id != track.value.id).toList();
.toList();
} else { } else {
selected.value = [...selected.value, track.value.id!]; selected.value = [...selected.value, track.value.id!];
} }
@ -187,17 +185,18 @@ class TracksTableView extends HookConsumerWidget {
if (checked == true) { if (checked == true) {
selected.value = [...selected.value, track.value.id!]; selected.value = [...selected.value, track.value.id!];
} else { } else {
selected.value = selected.value selected.value =
.where((id) => id != track.value.id) selected.value.where((id) => id != track.value.id).toList();
.toList();
} }
}, },
), ),
); );
}).toList(), }).toList(),
if (bottomSpace) const SizedBox(height: 70), if (bottomSpace) const SizedBox(height: 70),
], ];
), if (isSliver) {
); return SliverList(delegate: SliverChildListDelegate(children));
}
return ListView(children: children);
} }
} }

View File

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

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
final _paletteColorState = StateProvider<PaletteColor>( final _paletteColorState = StateProvider<PaletteColor>(
(ref) { (ref) {
@ -18,11 +19,10 @@ PaletteColor usePaletteColor(String imageUrl, WidgetRef ref) {
useEffect(() { useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final palette = await PaletteGenerator.fromImageProvider( final palette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider( UniversalImage.imageProvider(
imageUrl, imageUrl,
cacheKey: imageUrl, height: 50,
maxHeight: 50, width: 50,
maxWidth: 50,
), ),
); );
if (!mounted()) return; if (!mounted()) return;
@ -49,11 +49,10 @@ PaletteGenerator usePaletteGenerator(
useEffect(() { useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final newPalette = await PaletteGenerator.fromImageProvider( final newPalette = await PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider( UniversalImage.imageProvider(
imageUrl, imageUrl,
cacheKey: imageUrl, height: 50,
maxHeight: 50, width: 50,
maxWidth: 50,
), ),
); );
if (!mounted()) return; if (!mounted()) return;

View File

@ -61,12 +61,14 @@ class CurrentPlaylist {
String id; String id;
String name; String name;
String thumbnail; String thumbnail;
bool isLocal;
CurrentPlaylist({ CurrentPlaylist({
required this.tracks, required this.tracks,
required this.id, required this.id,
required this.name, required this.name,
required this.thumbnail, required this.thumbnail,
this.isLocal = false,
}); });
static CurrentPlaylist fromJson(Map<String, dynamic> map) { static CurrentPlaylist fromJson(Map<String, dynamic> map) {
@ -76,6 +78,7 @@ class CurrentPlaylist {
map["tracks"].map((track) => Track.fromJson(track)).toList()), map["tracks"].map((track) => Track.fromJson(track)).toList()),
name: map["name"], name: map["name"],
thumbnail: map["thumbnail"], thumbnail: map["thumbnail"],
isLocal: map["isLocal"],
); );
} }
@ -107,6 +110,7 @@ class CurrentPlaylist {
"name": name, "name": name,
"tracks": tracks.map((track) => track.toJson()).toList(), "tracks": tracks.map((track) => track.toJson()).toList(),
"thumbnail": thumbnail, "thumbnail": thumbnail,
"isLocal": isLocal,
}; };
} }
} }

143
lib/models/Id3Tags.dart Normal file
View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => Comment(
json["lang"] ?? "",
json["description"] ?? "",
json["comment"] ?? "",
);
Map<String, dynamic> toJson() => {
"comment": comment,
"description": description,
"key": key,
"lang": lang,
};
}
extension AttachedPictureJson on AttachedPicture {
static fromJson(Map<String, dynamic> json) => AttachedPicture(
json["mime"] ?? "",
json["imageTypeCode"] ?? 0,
json["description"] ?? "",
List<int>.from(json["imageData"] ?? []),
);
Map<String, dynamic> 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<String, dynamic> json) {
title = json['title'];
artist = json['artist'];
album = json['album'];
year = json['year'];
comment = json['comment'];
track = json['track'];
genre = json['genre'];
}
Map<String, dynamic> toJson() {
return {
'title': title,
'artist': artist,
'album': album,
'year': year,
'comment': comment,
'track': track,
'genre': genre,
};
}
}

View File

@ -1,17 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:queue/queue.dart'; import 'package:queue/queue.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:spotify/spotify.dart'; 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/Logger.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.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 queueInstance = Queue(delay: const Duration(seconds: 5));
Queue grabberQueue = Queue(delay: const Duration(seconds: 5)); Queue grabberQueue = Queue(delay: const Duration(seconds: 5));
@ -89,6 +95,54 @@ class Downloader with ChangeNotifier {
logger.v( logger.v(
"[addToQueue] Download of ${file.path} is done successfully", "[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<Artist>(
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) { } catch (e, stack) {
logger.e( logger.e(
"[addToQueue] Failed download of ${file.path}", "[addToQueue] Failed download of ${file.path}",

View File

@ -208,7 +208,10 @@ class Playback extends PersistedChangeNotifier {
artist: TypeConversionUtils.artists_X_String( artist: TypeConversionUtils.artists_X_String(
track.artists ?? <ArtistSimple>[]), track.artists ?? <ArtistSimple>[]),
artUri: Uri.parse( artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString(track.album?.images)), TypeConversionUtils.image_X_UrlString(
track.album?.images,
),
),
duration: track.ytTrack.duration, duration: track.ytTrack.duration,
); );
mobileAudioService?.addItem(tag); mobileAudioService?.addItem(tag);
@ -216,7 +219,11 @@ class Playback extends PersistedChangeNotifier {
this.track = track; this.track = track;
notifyListeners(); notifyListeners();
updatePersistence(); updatePersistence();
await player.play(UrlSource(track.ytUri)); await player.play(
track.ytUri.startsWith("http")
? UrlSource(track.ytUri)
: DeviceFileSource(track.ytUri),
);
status = PlaybackStatus.playing; status = PlaybackStatus.playing;
notifyListeners(); notifyListeners();
} catch (e, stack) { } catch (e, stack) {

View File

@ -2,11 +2,16 @@
import 'dart:io'; import 'dart:io';
import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/widgets.dart' hide Image; 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:spotube/components/Shared/LinkText.dart';
import 'package:spotify/spotify.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:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
abstract class TypeConversionUtils { abstract class TypeConversionUtils {
static String image_X_UrlString(List<Image>? images, {int index = 0}) { static String image_X_UrlString(List<Image>? images, {int index = 0}) {
@ -85,31 +90,61 @@ abstract class TypeConversionUtils {
return track; return track;
} }
static Track localTrack_X_Track(Metadata metadata, File file) { static SpotubeTrack localTrack_X_Track(
final track = Track(); List<Tag> 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() track.album = Album()
..name = metadata.albumName ..name = metadata.album ?? "Spotube"
..images = [if (art != null) Image()..url = art]
..genres = [if (metadata.genre != null) metadata.genre!] ..genres = [if (metadata.genre != null) metadata.genre!]
..artists = [ ..artists = [
Artist() Artist()
..name = metadata.albumArtistName ..name = metadata.tpe2 ?? "Spotube"
..id = metadata.albumArtistName ..id = metadata.tpe2 ?? "Spotube"
..type = "artist", ..type = "artist",
] ]
..id = "${metadata.albumName}${metadata.albumLength}"; ..id = metadata.album;
track.artists = metadata.trackArtistNames track.artists = [
?.map((name) => Artist() Artist()
..name = name ..name = metadata.tpe2 ?? "Spotube"
..id = name) ..id = metadata.tpe2 ?? "Spotube"
.toList(); ];
track.discNumber = metadata.discNumber; track.id = metadata.title ?? basenameWithoutExtension(file.path);
track.durationMs = metadata.trackDuration; track.name = metadata.title ?? basenameWithoutExtension(file.path);
track.id = "${metadata.trackName}${metadata.trackDuration}";
track.name = metadata.trackName;
track.trackNumber = metadata.trackNumber;
track.type = "track"; track.type = "track";
track.uri = file.path; track.uri = file.path;
track.durationMs = duration.inMilliseconds;
return track; return track;
} }

View File

@ -8,7 +8,6 @@
#include <audioplayers_linux/audioplayers_linux_plugin.h> #include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h> #include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <flutter_media_metadata/flutter_media_metadata_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
@ -18,9 +17,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); 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 = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux audioplayers_linux
bitsdojo_window_linux bitsdojo_window_linux
flutter_media_metadata
url_launcher_linux url_launcher_linux
) )

View File

@ -9,7 +9,6 @@ import audio_service
import audio_session import audio_session
import audioplayers_darwin import audioplayers_darwin
import bitsdojo_window_macos import bitsdojo_window_macos
import flutter_media_metadata
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import shared_preferences_macos import shared_preferences_macos
@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
FlutterMediaMetadataPlugin.register(with: registry.registrar(forPlugin: "FlutterMediaMetadataPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@ -140,7 +140,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.9.0"
audio_service: audio_service:
dependency: "direct main" dependency: "direct main"
description: description:
@ -357,14 +357,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -378,7 +371,7 @@ packages:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@ -442,6 +435,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.3" 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: dbus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -463,6 +463,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" 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: fading_edge_scrollview:
dependency: transitive dependency: transitive
description: description:
@ -476,7 +483,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -573,13 +580,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -702,6 +702,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.1"
id3:
dependency: "direct main"
description:
name: id3
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -778,21 +785,21 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.12"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.1.5"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.8.0"
mime: mime:
dependency: "direct main" dependency: "direct main"
description: description:
@ -800,6 +807,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" 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: msix:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -890,7 +904,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1175,7 +1189,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.9.0"
spotify: spotify:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1233,7 +1247,7 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:
@ -1247,14 +1261,14 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.9" version: "0.4.12"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1325,6 +1339,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" 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: uuid:
dependency: transitive dependency: transitive
description: description:

View File

@ -71,8 +71,9 @@ dependencies:
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
badges: ^2.0.3 badges: ^2.0.3
mime: ^1.0.2 mime: ^1.0.2
flutter_media_metadata: dart_tags: ^0.4.0
path: ../flutter_media_metadata id3: ^1.0.2
mp3_info: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -8,7 +8,6 @@
#include <audioplayers_windows/audioplayers_windows_plugin.h> #include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <flutter_media_metadata/flutter_media_metadata_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@ -17,8 +16,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BitsdojoWindowPluginRegisterWithRegistrar( BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
FlutterMediaMetadataPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterMediaMetadataPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows audioplayers_windows
bitsdojo_window_windows bitsdojo_window_windows
flutter_media_metadata
permission_handler_windows permission_handler_windows
url_launcher_windows url_launcher_windows
) )