mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: local library folder cards
This commit is contained in:
parent
d82261cb25
commit
fc5bfa089c
199
lib/components/library/local_folder/local_folder_item.dart
Normal file
199
lib/components/library/local_folder/local_folder_item.dart
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
|
class LocalFolderItem extends HookConsumerWidget {
|
||||||
|
final String folder;
|
||||||
|
const LocalFolderItem({super.key, required this.folder});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
final lerpValue = useBrightnessValue(.9, .7);
|
||||||
|
|
||||||
|
final downloadFolder =
|
||||||
|
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||||
|
|
||||||
|
final isDownloadFolder = folder == downloadFolder;
|
||||||
|
|
||||||
|
final Uri(:pathSegments) = Uri.parse(
|
||||||
|
folder
|
||||||
|
.replaceFirst(RegExp(r'^/Volumes/[^/]+/Users/'), "")
|
||||||
|
.replaceFirst(r'C:\Users\', "")
|
||||||
|
.replaceFirst(r'/home/', ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
// if length > 5, we ... all the middle segments after 2 and the last 2
|
||||||
|
final segments = pathSegments.length > 5
|
||||||
|
? [
|
||||||
|
...pathSegments.take(2),
|
||||||
|
"...",
|
||||||
|
...pathSegments.skip(pathSegments.length - 3).toList()
|
||||||
|
..removeLast(),
|
||||||
|
]
|
||||||
|
: pathSegments.take(pathSegments.length - 1).toList();
|
||||||
|
|
||||||
|
final trackSnapshot = ref.watch(
|
||||||
|
localTracksProvider.select(
|
||||||
|
(s) => s.whenData((tracks) => tracks[folder]?.take(4).toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tracks = trackSnapshot.value ?? [];
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (isDownloadFolder) {
|
||||||
|
context.go("/library/local?downloads=1", extra: folder);
|
||||||
|
} else {
|
||||||
|
context.go(
|
||||||
|
"/library/local",
|
||||||
|
extra: folder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Ink(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Color.lerp(
|
||||||
|
colorScheme.surfaceVariant,
|
||||||
|
colorScheme.surface,
|
||||||
|
lerpValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (tracks.isEmpty)
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
SpotubeIcons.folder,
|
||||||
|
size: mediaQuery.smAndDown
|
||||||
|
? 95
|
||||||
|
: mediaQuery.mdAndDown
|
||||||
|
? 100
|
||||||
|
: 142,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: max((tracks.length / 2).ceil(), 2),
|
||||||
|
),
|
||||||
|
itemCount: tracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final track = tracks[index];
|
||||||
|
return UniversalImage(
|
||||||
|
path: (track.album?.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
isDownloadFolder
|
||||||
|
? context.l10n.downloads
|
||||||
|
: basename(folder),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isDownloadFolder)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: PopupMenuButton(
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(3),
|
||||||
|
child: Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(SpotubeIcons.folderRemove),
|
||||||
|
iconColor: colorScheme.error,
|
||||||
|
title:
|
||||||
|
Text(context.l10n.remove_library_location),
|
||||||
|
onTap: () {
|
||||||
|
final libraryLocations = ref
|
||||||
|
.read(userPreferencesProvider)
|
||||||
|
.localLibraryLocation;
|
||||||
|
ref
|
||||||
|
.read(userPreferencesProvider.notifier)
|
||||||
|
.setLocalLibraryLocation(
|
||||||
|
libraryLocations
|
||||||
|
.where((e) => e != folder)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Wrap(
|
||||||
|
spacing: 2,
|
||||||
|
runSpacing: 2,
|
||||||
|
children: [
|
||||||
|
for (final MapEntry(key: index, value: segment)
|
||||||
|
in segments.asMap().entries)
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
if (index != 0)
|
||||||
|
TextSpan(
|
||||||
|
text: "/ ",
|
||||||
|
style: TextStyle(color: colorScheme.primary),
|
||||||
|
),
|
||||||
|
TextSpan(text: segment),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: colorScheme.tertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +1,18 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:file_selector/file_selector.dart';
|
import 'package:file_selector/file_selector.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:metadata_god/metadata_god.dart';
|
|
||||||
import 'package:mime/mime.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/library/local_folder/local_folder_item.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
|
||||||
|
|
||||||
const supportedAudioTypes = [
|
|
||||||
"audio/webm",
|
|
||||||
"audio/ogg",
|
|
||||||
"audio/mpeg",
|
|
||||||
"audio/mp4",
|
|
||||||
"audio/opus",
|
|
||||||
"audio/wav",
|
|
||||||
"audio/aac",
|
|
||||||
];
|
|
||||||
|
|
||||||
const imgMimeToExt = {
|
|
||||||
"image/png": ".png",
|
|
||||||
"image/jpeg": ".jpg",
|
|
||||||
"image/webp": ".webp",
|
|
||||||
"image/gif": ".gif",
|
|
||||||
};
|
|
||||||
|
|
||||||
enum SortBy {
|
enum SortBy {
|
||||||
none,
|
none,
|
||||||
@ -51,94 +25,6 @@ enum SortBy {
|
|||||||
album,
|
album,
|
||||||
}
|
}
|
||||||
|
|
||||||
final localTracksProvider =
|
|
||||||
FutureProvider<Map<String, List<LocalTrack>>>((ref) async {
|
|
||||||
try {
|
|
||||||
if (kIsWeb) return {};
|
|
||||||
final Map<String, List<LocalTrack>> tracks = {};
|
|
||||||
|
|
||||||
final downloadLocation = ref.watch(
|
|
||||||
userPreferencesProvider.select((s) => s.downloadLocation),
|
|
||||||
);
|
|
||||||
final downloadDir = Directory(downloadLocation);
|
|
||||||
if (!await downloadDir.exists()) {
|
|
||||||
await downloadDir.create(recursive: true);
|
|
||||||
}
|
|
||||||
final localLibraryLocations = ref.watch(
|
|
||||||
userPreferencesProvider.select((s) => s.localLibraryLocation),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var location in [downloadLocation, ...localLibraryLocations]) {
|
|
||||||
if (location.isEmpty) continue;
|
|
||||||
final entities = <FileSystemEntity>[];
|
|
||||||
if (await Directory(location).exists()) {
|
|
||||||
entities.addAll(Directory(location).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(
|
|
||||||
(file) async {
|
|
||||||
try {
|
|
||||||
final metadata = await MetadataGod.readMetadata(file: file.path);
|
|
||||||
|
|
||||||
final imageFile = File(join(
|
|
||||||
(await getTemporaryDirectory()).path,
|
|
||||||
"spotube",
|
|
||||||
basenameWithoutExtension(file.path) +
|
|
||||||
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
|
||||||
));
|
|
||||||
if (!await imageFile.exists() && metadata.picture != null) {
|
|
||||||
await imageFile.create(recursive: true);
|
|
||||||
await imageFile.writeAsBytes(
|
|
||||||
metadata.picture?.data ?? [],
|
|
||||||
mode: FileMode.writeOnly,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"metadata": metadata,
|
|
||||||
"file": file,
|
|
||||||
"art": imageFile.path
|
|
||||||
};
|
|
||||||
} catch (e, stack) {
|
|
||||||
if (e is FfiException) {
|
|
||||||
return {"file": file};
|
|
||||||
}
|
|
||||||
Catcher2.reportCheckedError(e, stack);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.where((e) => e.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// ignore: no_leading_underscores_for_local_identifiers
|
|
||||||
final _tracks = filesWithMetadata
|
|
||||||
.map(
|
|
||||||
(fileWithMetadata) => LocalTrack.fromTrack(
|
|
||||||
track: Track().fromFile(
|
|
||||||
fileWithMetadata["file"],
|
|
||||||
metadata: fileWithMetadata["metadata"],
|
|
||||||
art: fileWithMetadata["art"],
|
|
||||||
),
|
|
||||||
path: fileWithMetadata["file"].path,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
tracks[location] = _tracks;
|
|
||||||
}
|
|
||||||
return tracks;
|
|
||||||
} catch (e, stack) {
|
|
||||||
Catcher2.reportCheckedError(e, stack);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class UserLocalTracks extends HookConsumerWidget {
|
class UserLocalTracks extends HookConsumerWidget {
|
||||||
const UserLocalTracks({super.key});
|
const UserLocalTracks({super.key});
|
||||||
|
|
||||||
@ -167,61 +53,49 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}, [preferences.localLibraryLocation]);
|
}, [preferences.localLibraryLocation]);
|
||||||
|
|
||||||
final removeLocalLibraryLocation = useCallback((String location) {
|
|
||||||
if (!preferences.localLibraryLocation.contains(location)) return;
|
|
||||||
preferencesNotifier.setLocalLibraryLocation(
|
|
||||||
[...preferences.localLibraryLocation]..remove(location),
|
|
||||||
);
|
|
||||||
}, [preferences.localLibraryLocation]);
|
|
||||||
|
|
||||||
// This is just to pre-load the tracks.
|
// This is just to pre-load the tracks.
|
||||||
// For now, this gets all of them.
|
// For now, this gets all of them.
|
||||||
ref.watch(localTracksProvider);
|
ref.watch(localTracksProvider);
|
||||||
|
|
||||||
return Column(children: [
|
return LayoutBuilder(builder: (context, constrains) {
|
||||||
Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
child: Row(children: [
|
child: Column(
|
||||||
const SizedBox(width: 5),
|
children: [
|
||||||
TextButton.icon(
|
Align(
|
||||||
icon: const Icon(SpotubeIcons.folderAdd),
|
alignment: Alignment.centerRight,
|
||||||
label: Text(context.l10n.add_library_location),
|
child: TextButton.icon(
|
||||||
onPressed: addLocalLibraryLocation,
|
icon: const Icon(SpotubeIcons.folderAdd),
|
||||||
)
|
label: Text(context.l10n.add_library_location),
|
||||||
])),
|
onPressed: addLocalLibraryLocation,
|
||||||
Expanded(
|
),
|
||||||
child: ListView.builder(
|
),
|
||||||
itemCount: preferences.localLibraryLocation.length + 1,
|
const Gap(8),
|
||||||
itemBuilder: (context, index) {
|
Expanded(
|
||||||
late final String location;
|
child: GridView.builder(
|
||||||
if (index == 0) {
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
location = preferences.downloadLocation;
|
maxCrossAxisExtent: 200,
|
||||||
} else {
|
mainAxisExtent: constrains.isXs
|
||||||
location = preferences.localLibraryLocation[index - 1];
|
? 210
|
||||||
}
|
: constrains.mdAndDown
|
||||||
return ListTile(
|
? 280
|
||||||
title: preferences.downloadLocation != location
|
: 250,
|
||||||
? Text(location)
|
crossAxisSpacing: 10,
|
||||||
: Text(context.l10n.downloads),
|
mainAxisSpacing: 10,
|
||||||
trailing: preferences.downloadLocation != location
|
),
|
||||||
? Tooltip(
|
itemCount: preferences.localLibraryLocation.length + 1,
|
||||||
message: context.l10n.remove_library_location,
|
itemBuilder: (context, index) {
|
||||||
child: IconButton(
|
return LocalFolderItem(
|
||||||
icon: Icon(SpotubeIcons.folderRemove,
|
folder: index == 0
|
||||||
color: Colors.red[400]),
|
? preferences.downloadLocation
|
||||||
onPressed: () =>
|
: preferences.localLibraryLocation[index - 1],
|
||||||
removeLocalLibraryLocation(location),
|
);
|
||||||
),
|
},
|
||||||
)
|
),
|
||||||
: null,
|
),
|
||||||
onTap: () async {
|
],
|
||||||
context.go(
|
),
|
||||||
"/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}",
|
);
|
||||||
extra: location,
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||||
@ -23,6 +22,7 @@ import 'package:spotube/models/local_track.dart';
|
|||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
@ -3,8 +3,8 @@ import 'package:device_info_plus/device_info_plus.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:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
|
||||||
import 'package:spotube/hooks/utils/use_async_effect.dart';
|
import 'package:spotube/hooks/utils/use_async_effect.dart';
|
||||||
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
void useGetStoragePermissions(WidgetRef ref) {
|
void useGetStoragePermissions(WidgetRef ref) {
|
||||||
|
@ -16,6 +16,7 @@ import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
|||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
125
lib/provider/local_tracks/local_tracks_provider.dart
Normal file
125
lib/provider/local_tracks/local_tracks_provider.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||||
|
|
||||||
|
const supportedAudioTypes = [
|
||||||
|
"audio/webm",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/mp4",
|
||||||
|
"audio/opus",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/aac",
|
||||||
|
];
|
||||||
|
|
||||||
|
const imgMimeToExt = {
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
};
|
||||||
|
|
||||||
|
final localTracksProvider =
|
||||||
|
FutureProvider<Map<String, List<LocalTrack>>>((ref) async {
|
||||||
|
try {
|
||||||
|
if (kIsWeb) return {};
|
||||||
|
final Map<String, List<LocalTrack>> tracks = {};
|
||||||
|
|
||||||
|
final downloadLocation = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.downloadLocation),
|
||||||
|
);
|
||||||
|
final downloadDir = Directory(downloadLocation);
|
||||||
|
if (!await downloadDir.exists()) {
|
||||||
|
await downloadDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
final localLibraryLocations = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.localLibraryLocation),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var location in [downloadLocation, ...localLibraryLocations]) {
|
||||||
|
if (location.isEmpty) continue;
|
||||||
|
final entities = <FileSystemEntity>[];
|
||||||
|
if (await Directory(location).exists()) {
|
||||||
|
try {
|
||||||
|
entities.addAll(Directory(location).listSync(recursive: true));
|
||||||
|
} catch (e, stack) {
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
(file) async {
|
||||||
|
try {
|
||||||
|
final metadata = await MetadataGod.readMetadata(file: file.path);
|
||||||
|
|
||||||
|
final imageFile = File(join(
|
||||||
|
(await getTemporaryDirectory()).path,
|
||||||
|
"spotube",
|
||||||
|
basenameWithoutExtension(file.path) +
|
||||||
|
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
||||||
|
));
|
||||||
|
if (!await imageFile.exists() && metadata.picture != null) {
|
||||||
|
await imageFile.create(recursive: true);
|
||||||
|
await imageFile.writeAsBytes(
|
||||||
|
metadata.picture?.data ?? [],
|
||||||
|
mode: FileMode.writeOnly,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"metadata": metadata,
|
||||||
|
"file": file,
|
||||||
|
"art": imageFile.path
|
||||||
|
};
|
||||||
|
} catch (e, stack) {
|
||||||
|
if (e is FfiException) {
|
||||||
|
return {"file": file};
|
||||||
|
}
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// ignore: no_leading_underscores_for_local_identifiers
|
||||||
|
final _tracks = filesWithMetadata
|
||||||
|
.map(
|
||||||
|
(fileWithMetadata) => LocalTrack.fromTrack(
|
||||||
|
track: Track().fromFile(
|
||||||
|
fileWithMetadata["file"],
|
||||||
|
metadata: fileWithMetadata["metadata"],
|
||||||
|
art: fileWithMetadata["art"],
|
||||||
|
),
|
||||||
|
path: fileWithMetadata["file"].path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
tracks[location] = _tracks;
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
} catch (e, stack) {
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
@ -71,8 +71,10 @@ class ProxyPlaylist {
|
|||||||
/// To make sure proper instance method is used for JSON serialization
|
/// To make sure proper instance method is used for JSON serialization
|
||||||
/// Otherwise default super.toJson() is used
|
/// Otherwise default super.toJson() is used
|
||||||
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
|
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
|
||||||
return switch (track.runtimeType) {
|
return switch (track) {
|
||||||
|
// ignore: unnecessary_cast
|
||||||
LocalTrack() => (track as LocalTrack).toJson(),
|
LocalTrack() => (track as LocalTrack).toJson(),
|
||||||
|
// ignore: unnecessary_cast
|
||||||
SourcedTrack() => (track as SourcedTrack).toJson(),
|
SourcedTrack() => (track as SourcedTrack).toJson(),
|
||||||
_ => track.toJson(),
|
_ => track.toJson(),
|
||||||
};
|
};
|
||||||
|
@ -41,6 +41,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- system_theme (0.0.1):
|
- system_theme (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- system_tray (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- tray_manager (0.0.1):
|
- tray_manager (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
@ -70,6 +72,7 @@ DEPENDENCIES:
|
|||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||||
- system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`)
|
- system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`)
|
||||||
|
- system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`)
|
||||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||||
@ -118,6 +121,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||||
system_theme:
|
system_theme:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos
|
||||||
|
system_tray:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos
|
||||||
tray_manager:
|
tray_manager:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
@ -148,6 +153,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc
|
system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc
|
||||||
|
system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d
|
||||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||||
|
Loading…
Reference in New Issue
Block a user