mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'feature_organized_settings' of https://github.com/Demizo/spotube into feature_organized_settings
This commit is contained in:
commit
1263a0cfcf
BIN
assets/album-placeholder.png
Normal file
BIN
assets/album-placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
BIN
assets/user-placeholder.png
Normal file
BIN
assets/user-placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -156,6 +156,13 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
CachedNetworkImageProvider(avatarImg),
|
CachedNetworkImageProvider(avatarImg),
|
||||||
|
onBackgroundImageError:
|
||||||
|
(exception, stackTrace) =>
|
||||||
|
Image.asset(
|
||||||
|
"assets/user-placeholder.png",
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 10,
|
width: 10,
|
||||||
@ -187,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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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>[],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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()),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,54 +31,193 @@ 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>>([]);
|
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(() {
|
final imageFile = mp3Instance.parseTagsSync()
|
||||||
(() async {
|
? File(join(
|
||||||
if (!await downloadDir.exists()) {
|
(await getTemporaryDirectory()).path,
|
||||||
await downloadDir.create(recursive: true);
|
"spotube",
|
||||||
return;
|
basenameWithoutExtension(f.path) +
|
||||||
}
|
imgMimeToExt[
|
||||||
final entities = downloadDir.listSync(recursive: true);
|
mp3Instance.metaTags["APIC"]?["mime"] ?? "image/jpeg"]!,
|
||||||
final filesWithMetadata = (await Future.wait(
|
))
|
||||||
entities.map((e) => File(e.path)).where((file) {
|
: null;
|
||||||
final mimetype = lookupMimeType(file.path);
|
if (imageFile != null &&
|
||||||
return mimetype != null && supportedAudioTypes.contains(mimetype);
|
!await imageFile.exists() &&
|
||||||
}).map(
|
mp3Instance.metaTags["APIC"]?["base64"] != null) {
|
||||||
(f) async => {
|
await imageFile.create(recursive: true);
|
||||||
"metadata": await MetadataRetriever.fromFile(f),
|
await imageFile.writeAsBytes(
|
||||||
"file": f,
|
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<Tag>,
|
||||||
|
fileWithMetadata["file"] as File,
|
||||||
|
fileWithMetadata["duration"] as Duration,
|
||||||
|
fileWithMetadata["art"] as String?,
|
||||||
),
|
),
|
||||||
));
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final tracks = filesWithMetadata
|
return tracks;
|
||||||
.map(
|
});
|
||||||
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
|
||||||
fileWithMetadata["metadata"] as Metadata,
|
|
||||||
fileWithMetadata["file"] as File),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
localTracks.value = 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()),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
playback.track?.album?.images,
|
? TypeConversionUtils.image_X_UrlString(
|
||||||
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
playback.track?.album?.images,
|
||||||
),
|
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
||||||
|
)
|
||||||
|
: "assets/album-placeholder.png",
|
||||||
[playback.track?.album?.images],
|
[playback.track?.album?.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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 Container(
|
return Image.asset(
|
||||||
|
"assets/album-placeholder.png",
|
||||||
height: 50,
|
height: 50,
|
||||||
width: 50,
|
width: 50,
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
path: thumbnailUrl!,
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
return Container(
|
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,61 +255,70 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
TypeConversionUtils.artists_X_ClickableArtists(
|
isReallyLocal
|
||||||
track.value.artists ?? [],
|
? Text(
|
||||||
textStyle: TextStyle(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
fontSize:
|
track.value.artists ?? []),
|
||||||
breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)),
|
)
|
||||||
|
: TypeConversionUtils.artists_X_ClickableArtists(
|
||||||
|
track.value.artists ?? [],
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontSize: breakpoint.isLessThan(Breakpoints.lg)
|
||||||
|
? 12
|
||||||
|
: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
|
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LinkText(
|
child: isReallyLocal
|
||||||
track.value.album!.name!,
|
? Text(track.value.album?.name ?? "")
|
||||||
"/album/${track.value.album?.id}",
|
: LinkText(
|
||||||
extra: track.value.album,
|
track.value.album!.name!,
|
||||||
overflow: TextOverflow.ellipsis,
|
"/album/${track.value.album?.id}",
|
||||||
),
|
extra: track.value.album,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!breakpoint.isSm) ...[
|
if (!breakpoint.isSm) ...[
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(duration),
|
Text(duration),
|
||||||
],
|
],
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
AdaptiveActions(
|
if (!isReallyLocal)
|
||||||
actions: [
|
AdaptiveActions(
|
||||||
if (auth.isLoggedIn)
|
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(
|
Action(
|
||||||
icon: Icon(isSaved
|
icon: const Icon(Icons.share_rounded),
|
||||||
? Icons.favorite_rounded
|
text: const Text("Share"),
|
||||||
: Icons.favorite_border_rounded),
|
|
||||||
text: const Text("Save as favorite"),
|
|
||||||
onPressed: () {
|
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);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,156 +50,153 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
[tracks],
|
[tracks],
|
||||||
);
|
);
|
||||||
|
|
||||||
return SliverList(
|
final children = [
|
||||||
delegate: SliverChildListDelegate(
|
if (heading != null) heading!,
|
||||||
[
|
Row(
|
||||||
if (heading != null) heading!,
|
children: [
|
||||||
Row(
|
Checkbox(
|
||||||
children: [
|
value: selected.value.length == tracks.length,
|
||||||
Checkbox(
|
onChanged: (checked) {
|
||||||
value: selected.value.length == tracks.length,
|
if (!showCheck.value) showCheck.value = true;
|
||||||
onChanged: (checked) {
|
if (checked == true) {
|
||||||
if (!showCheck.value) showCheck.value = true;
|
selected.value = tracks.map((s) => s.id!).toList();
|
||||||
if (checked == true) {
|
} else {
|
||||||
selected.value = tracks.map((s) => s.id!).toList();
|
selected.value = [];
|
||||||
} else {
|
showCheck.value = false;
|
||||||
selected.value = [];
|
}
|
||||||
showCheck.value = false;
|
},
|
||||||
}
|
),
|
||||||
},
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.all(8.0),
|
||||||
Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.all(8.0),
|
"#",
|
||||||
child: Text(
|
textAlign: TextAlign.center,
|
||||||
"#",
|
style: tableHeadStyle,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Title",
|
||||||
style: tableHeadStyle,
|
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: [
|
if (!breakpoint.isSm) ...[
|
||||||
Text(
|
const SizedBox(width: 10),
|
||||||
"Title",
|
Text("Time", style: tableHeadStyle),
|
||||||
style: tableHeadStyle,
|
const SizedBox(width: 10),
|
||||||
overflow: TextOverflow.ellipsis,
|
],
|
||||||
),
|
PopupMenuButton(
|
||||||
],
|
itemBuilder: (context) {
|
||||||
),
|
return [
|
||||||
),
|
PopupMenuItem(
|
||||||
// used alignment of this table-head
|
enabled: selected.value.isNotEmpty,
|
||||||
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
|
|
||||||
const SizedBox(width: 100),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
const Icon(Icons.file_download_outlined),
|
||||||
Text(
|
Text(
|
||||||
"Album",
|
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: tableHeadStyle,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
value: "download",
|
||||||
],
|
),
|
||||||
if (!breakpoint.isSm) ...[
|
];
|
||||||
const SizedBox(width: 10),
|
},
|
||||||
Text("Time", style: tableHeadStyle),
|
onSelected: (action) async {
|
||||||
const SizedBox(width: 10),
|
switch (action) {
|
||||||
],
|
case "download":
|
||||||
PopupMenuButton(
|
{
|
||||||
itemBuilder: (context) {
|
final isConfirmed = await showDialog(
|
||||||
return [
|
context: context,
|
||||||
PopupMenuItem(
|
builder: (context) {
|
||||||
enabled: selected.value.isNotEmpty,
|
return const DownloadConfirmationDialog();
|
||||||
child: Row(
|
});
|
||||||
children: [
|
if (isConfirmed != true) return;
|
||||||
const Icon(Icons.file_download_outlined),
|
for (final selectedTrack in selectedTracks) {
|
||||||
Text(
|
downloader.addToQueue(selectedTrack);
|
||||||
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
|
}
|
||||||
),
|
selected.value = [];
|
||||||
],
|
showCheck.value = false;
|
||||||
),
|
break;
|
||||||
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:
|
|
||||||
}
|
}
|
||||||
},
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
98
lib/components/Shared/UniversalImage.dart
Normal file
98
lib/components/Shared/UniversalImage.dart
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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
143
lib/models/Id3Tags.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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}",
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
73
pubspec.lock
73
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user