diff --git a/analysis_options.yaml b/analysis_options.yaml index d5b904cc..1eda286e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -39,3 +39,4 @@ analyzer: - "**.g.dart" - "**.gr.dart" - "**/generated_plugin_registrant.dart" + - test/**/*.dart diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 7c2660e6..5c4df85f 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -125,4 +125,6 @@ abstract class SpotubeIcons { static const folderAdd = FeatherIcons.folderPlus; static const folderRemove = FeatherIcons.folderMinus; static const cache = FeatherIcons.hardDrive; + static const export = Icons.file_open_outlined; + static const delete = FeatherIcons.trash2; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 60198259..f949480e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -394,5 +394,12 @@ "unsupported_platform": "Unsupported platform", "cache_music": "Cache music", "open": "Open", - "cache_folder": "Cache folder" + "cache_folder": "Cache folder", + "export": "Export", + "clear_cache": "Clear cache", + "clear_cache_confirmation": "Do you want to clear the cache?", + "export_cache_files": "Export Cached Files", + "found_n_files": "Found {count} files", + "export_cache_confirmation": "Do you want to export these files to", + "exported_n_out_of_m_files": "Exported {filesExported} out of {files} files" } \ No newline at end of file diff --git a/lib/modules/library/local_folder/cache_export_dialog.dart b/lib/modules/library/local_folder/cache_export_dialog.dart new file mode 100644 index 00000000..1d1421be --- /dev/null +++ b/lib/modules/library/local_folder/cache_export_dialog.dart @@ -0,0 +1,139 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; + +final codecs = SourceCodecs.values.map((s) => s.name); + +class LocalFolderCacheExportDialog extends HookConsumerWidget { + final Directory exportDir; + final Directory cacheDir; + const LocalFolderCacheExportDialog({ + super.key, + required this.exportDir, + required this.cacheDir, + }); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme, :colorScheme) = Theme.of(context); + + final files = useState>([]); + final filesExported = useState(0); + + useEffect(() { + final stream = cacheDir.list().where( + (event) => + event is File && + codecs.contains(extension(event.path).replaceAll(".", "")), + ); + + stream.listen( + (event) { + files.value = [...files.value, event as File]; + }, + onError: (e, stack) { + AppLogger.reportError(e, stack); + }, + ); + return null; + }, []); + + useEffect(() { + if (filesExported.value == files.value.length && + filesExported.value > 0) { + Navigator.of(context).pop(); + } + return null; + }, [filesExported.value, files.value]); + + final isExportInProgress = + filesExported.value > 0 && filesExported.value != files.value.length; + + return AlertDialog( + title: Text(context.l10n.export_cache_files), + content: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: filesExported.value == 0 + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.found_n_files(files.value.length.toString()), + ), + const Gap(10), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: context.l10n.export_cache_confirmation, + ), + TextSpan( + text: "\n${exportDir.path}?", + style: textTheme.labelMedium!.copyWith( + color: colorScheme.secondary, + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.exported_n_out_of_m_files( + files.value.length.toString(), + filesExported.value.toString(), + ), + ), + const Gap(10), + LinearProgressIndicator( + value: filesExported.value / files.value.length, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: isExportInProgress + ? null + : () { + Navigator.of(context).pop(); + }, + child: Text(context.l10n.cancel), + ), + TextButton( + onPressed: isExportInProgress + ? null + : () async { + for (final file in files.value) { + try { + final destinationFile = File( + join(exportDir.path, basename(file.path)), + ); + + if (await destinationFile.exists()) { + await destinationFile.delete(); + } + await file.copy(destinationFile.path); + filesExported.value++; + } catch (e, stack) { + AppLogger.reportError(e, stack); + continue; + } + } + }, + child: Text(context.l10n.export), + ), + ], + ); + } +} diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index c48b0df8..3be45ee6 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -1,4 +1,8 @@ +import 'dart:io'; +import 'dart:math'; + import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; @@ -7,6 +11,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/string.dart'; +import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart'; import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/fallbacks/not_found.dart'; @@ -19,6 +24,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/service_utils.dart'; class LocalLibraryPage extends HookConsumerWidget { @@ -59,6 +65,8 @@ class LocalLibraryPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final ThemeData(:textTheme) = Theme.of(context); + final sortBy = useState(SortBy.none); final playlist = ref.watch(audioPlayerProvider); final trackSnapshot = ref.watch(localTracksProvider); @@ -72,20 +80,131 @@ class LocalLibraryPage extends HookConsumerWidget { final controller = useScrollController(); + final directorySize = useMemoized(() async { + final dir = Directory(location); + final files = await dir.list(recursive: true).toList(); + + final filesLength = + await Future.wait(files.whereType().map((e) => e.length())); + + return (filesLength.sum.toInt() / pow(10, 9)).toStringAsFixed(2); + }, [location]); + return SafeArea( bottom: false, child: Scaffold( appBar: PageWindowTitleBar( leading: const BackButton(), centerTitle: true, - title: Text( - isDownloads - ? context.l10n.downloads - : isCache - ? context.l10n.cache_folder.capitalize() - : location, + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isDownloads + ? context.l10n.downloads + : isCache + ? context.l10n.cache_folder.capitalize() + : location, + style: textTheme.titleLarge, + ), + FutureBuilder( + future: directorySize, + builder: (context, snapshot) { + return Text( + "${(snapshot.data ?? 0)} GB", + style: textTheme.labelSmall, + ); + }, + ) + ], ), backgroundColor: Colors.transparent, + actions: [ + if (isCache) ...[ + IconButton( + icon: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.delete), + Text( + context.l10n.clear_cache, + style: textTheme.labelSmall, + ) + ], + ), + onPressed: () async { + final accepted = await showDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: Text(context.l10n.clear_cache_confirmation), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.decline), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(true); + }, + child: Text(context.l10n.accept), + ), + ], + ), + ); + + if (accepted ?? false) return; + + final cacheDir = Directory( + await UserPreferencesNotifier.getMusicCacheDir(), + ); + + if (cacheDir.existsSync()) { + await cacheDir.delete(recursive: true); + } + }, + ), + IconButton( + icon: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.export), + Text( + context.l10n.export, + style: textTheme.labelSmall, + ) + ], + ), + onPressed: () async { + final exportPath = + await FilePicker.platform.getDirectoryPath(); + + if (exportPath == null) return; + final exportDirectory = Directory(exportPath); + + if (!exportDirectory.existsSync()) { + await exportDirectory.create(recursive: true); + } + + final cacheDir = Directory( + await UserPreferencesNotifier.getMusicCacheDir()); + + if (!context.mounted) return; + await showDialog( + context: context, + builder: (context) { + return LocalFolderCacheExportDialog( + cacheDir: cacheDir, + exportDir: exportDirectory, + ); + }, + ); + }, + ), + ] + ], ), body: Column( children: [ diff --git a/untranslated_messages.json b/untranslated_messages.json index 2d4fa49a..9cbff978 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -6,7 +6,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "bn": [ @@ -16,7 +23,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ca": [ @@ -26,7 +40,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "cs": [ @@ -36,7 +57,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "de": [ @@ -46,7 +74,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "es": [ @@ -56,7 +91,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "eu": [ @@ -66,7 +108,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "fa": [ @@ -76,7 +125,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "fi": [ @@ -86,7 +142,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "fr": [ @@ -96,7 +159,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "hi": [ @@ -106,7 +176,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "id": [ @@ -116,7 +193,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "it": [ @@ -126,7 +210,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ja": [ @@ -136,7 +227,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ka": [ @@ -146,7 +244,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ko": [ @@ -156,7 +261,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ne": [ @@ -166,7 +278,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "nl": [ @@ -176,7 +295,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "pl": [ @@ -186,7 +312,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "pt": [ @@ -196,7 +329,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "ru": [ @@ -206,7 +346,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "th": [ @@ -216,7 +363,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "tr": [ @@ -226,7 +380,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "uk": [ @@ -236,7 +397,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "vi": [ @@ -246,7 +414,14 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ], "zh": [ @@ -256,6 +431,13 @@ "invidious_source_description", "cache_music", "open", - "cache_folder" + "cache_folder", + "export", + "clear_cache", + "clear_cache_confirmation", + "export_cache_files", + "found_n_files", + "export_cache_confirmation", + "exported_n_out_of_m_files" ] }