diff --git a/lib/controller/indexer_controller.dart b/lib/controller/indexer_controller.dart index 58e0b761..24f407d8 100644 --- a/lib/controller/indexer_controller.dart +++ b/lib/controller/indexer_controller.dart @@ -1427,21 +1427,7 @@ class Indexer { await _updateDirectoryStats(AppDirs.PALETTES, colorPalettesInStorage, null); } - Future updateVideosSizeInStorage({String? newVideoPath, File? oldDeletedFile}) async { - if (newVideoPath != null || oldDeletedFile != null) { - if (oldDeletedFile != null) { - if (await oldDeletedFile.exists()) { - videosInStorage.value--; - videosInStorage.value -= await oldDeletedFile.length(); - } - } - if (newVideoPath != null) { - videosInStorage.value++; - videosInStorage.value += await File(newVideoPath).length(); - } - - return; - } + Future updateVideosSizeInStorage() async { await _updateDirectoryStats(AppDirs.VIDEOS_CACHE, videosInStorage, videosSizeInStorage); } @@ -1471,13 +1457,19 @@ class Indexer { /// Deletes specific videos or the whole cache. Future clearVideoCache([List? videosToDelete]) async { if (videosToDelete != null) { - await videosToDelete.loopFuture((v, index) async => await File(v.path).delete()); + for (final v in videosToDelete) { + final deleted = await File(v.path).tryDeleting(); + if (deleted) { + videosInStorage.value--; + videosSizeInStorage.value -= v.sizeInBytes; + } + } } else { await Directory(AppDirs.VIDEOS_CACHE).delete(recursive: true); await Directory(AppDirs.VIDEOS_CACHE).create(); + videosInStorage.value = 0; + videosSizeInStorage.value = 0; } - - updateVideosSizeInStorage(); } Future _createDefaultNamidaArtwork() async { diff --git a/lib/controller/queue_controller.dart b/lib/controller/queue_controller.dart index c96c5864..37a94266 100644 --- a/lib/controller/queue_controller.dart +++ b/lib/controller/queue_controller.dart @@ -263,7 +263,7 @@ class QueueController { } Future _deleteQueueFromStorage(Queue queue) async { - await File('${AppDirs.QUEUES}${queue.date}.json').delete(); + await File('${AppDirs.QUEUES}${queue.date}.json').tryDeleting(); } /// Used to add Queues that were rejected by [addNewQueue] after full loading of queues. diff --git a/lib/controller/video_controller.dart b/lib/controller/video_controller.dart index 5915fd37..c64ba95c 100644 --- a/lib/controller/video_controller.dart +++ b/lib/controller/video_controller.dart @@ -222,6 +222,17 @@ class VideoController { return videos; } + void removeNVFromCacheMap(String youtubeId, String path) { + _videoCacheIDMap[youtubeId]?.removeWhere((element) { + if (element.path == path) { + Indexer.inst.videosInStorage.value--; + Indexer.inst.videosSizeInStorage.value -= element.sizeInBytes; + return true; + } + return false; + }); + } + Future updateCurrentVideo(Track track, {bool returnEarly = false}) async { isNoVideosAvailable.value = false; currentDownloadedBytes.value = null; diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index dd75ee29..120f8fcc 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -745,7 +745,7 @@ class _NamidaMiniPlayerState extends State { bgColor: isCurrent ? CurrentColor.inst.miniplayerColor.withAlpha(20) : null, icon: Broken.video, title: [ - "${element.height}p${element.framerateText()}", + "${element.resolution}p${element.framerateText()}", localOrCache, ].join(' • '), subtitle: [ @@ -800,7 +800,7 @@ class _NamidaMiniPlayerState extends State { final currentVideo = VideoController.inst.currentVideo.value; final downloadedBytes = VideoController.inst.currentDownloadedBytes.value; final videoTotalSize = currentVideo?.sizeInBytes ?? 0; - final videoQuality = currentVideo?.height ?? 0; + final videoQuality = currentVideo?.resolution ?? 0; final videoFramerate = currentVideo?.framerateText(30); final markText = VideoController.inst.isNoVideosAvailable.value ? 'x' : '?'; final fallbackQualityLabel = currentVideo?.nameInCache?.split('_').last; diff --git a/lib/ui/widgets/custom_widgets.dart b/lib/ui/widgets/custom_widgets.dart index 55ef2876..1982c99b 100644 --- a/lib/ui/widgets/custom_widgets.dart +++ b/lib/ui/widgets/custom_widgets.dart @@ -678,6 +678,8 @@ class SmallListTile extends StatelessWidget { final double? titleGap; final double borderRadius; final Widget? leading; + final VisualDensity? visualDensity; + const SmallListTile({ super.key, required this.title, @@ -694,6 +696,7 @@ class SmallListTile extends StatelessWidget { this.titleGap, this.borderRadius = 0.0, this.leading, + this.visualDensity, }); @override @@ -716,7 +719,7 @@ class SmallListTile extends StatelessWidget { size: 18.0, ), ), - visualDensity: compact ? const VisualDensity(horizontal: -2.0, vertical: -2.0) : const VisualDensity(horizontal: -1.0, vertical: -1.0), + visualDensity: visualDensity ?? (compact ? const VisualDensity(horizontal: -2.0, vertical: -2.0) : const VisualDensity(horizontal: -1.0, vertical: -1.0)), title: Text( title, style: context.textTheme.displayMedium?.copyWith( diff --git a/lib/ui/widgets/settings/advanced_settings.dart b/lib/ui/widgets/settings/advanced_settings.dart index af0c0b54..c34de1ba 100644 --- a/lib/ui/widgets/settings/advanced_settings.dart +++ b/lib/ui/widgets/settings/advanced_settings.dart @@ -622,7 +622,7 @@ class AdvancedSettings extends SettingSubpageProvider { style: context.textTheme.displayMedium, ), Text( - "${video.height}p • ${video.framerate}fps - ${video.sizeInBytes.fileSizeFormatted}", + "${video.resolution}p • ${video.framerate}fps - ${video.sizeInBytes.fileSizeFormatted}", style: context.textTheme.displaySmall, ), ], diff --git a/lib/ui/widgets/video_widget.dart b/lib/ui/widgets/video_widget.dart index c1b5fb26..2290eaf9 100644 --- a/lib/ui/widgets/video_widget.dart +++ b/lib/ui/widgets/video_widget.dart @@ -982,14 +982,16 @@ class NamidaVideoControlsState extends State with TickerPro ), ), ...ytQualities.map((element) { + final sizeInBytes = element.sizeInBytes; return Obx( () { final isSelected = element.height == Player.inst.currentVideoStream?.height; final id = Player.inst.nowPlayingVideoID?.id; final cachedFile = id == null ? null : element.getCachedFile(id); + return _getQualityChip( title: element.resolution ?? '', - subtitle: " • ${element.sizeInBytes?.fileSizeFormatted ?? ''}", + subtitle: sizeInBytes == null ? '' : " • ${sizeInBytes.fileSizeFormatted}", onPlay: (isSelected) { if (!isSelected) { Player.inst.onItemPlayYoutubeIDSetQuality( diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 0a99e6ae..7b426025 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -272,6 +272,15 @@ class YoutubeMiniPlayer extends StatelessWidget { ); items.add(repeatForWidget); } + items.add( + NamidaPopupItem( + icon: Broken.trash, + title: lang.CLEAR, + onTap: () { + YTUtils().showVideoClearDialog(context, videoId, CurrentColor.inst.miniplayerColor); + }, + ), + ); return items; }, child: const Icon( diff --git a/lib/youtube/yt_utils.dart b/lib/youtube/yt_utils.dart index 82d18688..95a88f39 100644 --- a/lib/youtube/yt_utils.dart +++ b/lib/youtube/yt_utils.dart @@ -8,6 +8,7 @@ import 'package:newpipeextractor_dart/newpipeextractor_dart.dart'; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:namida/class/video.dart'; import 'package:namida/controller/ffmpeg_controller.dart'; import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -377,4 +378,200 @@ class YTUtils { ); } } + + void showVideoClearDialog(BuildContext context, String videoId, Color colorScheme) { + final videosCached = VideoController.inst.getNVFromID(videoId); + final audiosCached = Player.inst.audioCacheMap[videoId]?.where((element) => element.file.existsSync()).toList() ?? []; + + final fileSizeLookup = {}; + final fileTypeLookup = {}; + + int videosSize = 0; + int audiosSize = 0; + + audiosCached.loop((e, _) { + final s = e.file.sizeInBytesSync(); + audiosSize += s; + fileSizeLookup[e.file.path] = s; + fileTypeLookup[e.file.path] = 0; + }); + videosCached.loop((e, _) { + final s = e.sizeInBytes; + videosSize += s; + fileSizeLookup[e.path] = s; + fileTypeLookup[e.path] = 1; + }); + + final pathsToDelete = {}.obs; + final allSelected = false.obs; + final totalSizeToDelete = 0.obs; + + Future deleteItems(Iterable paths) async { + for (final path in paths) { + await File(path).tryDeleting(); + + final type = fileTypeLookup[path]; + if (type == 1) { + VideoController.inst.removeNVFromCacheMap(videoId, path); + } else if (type == 0) { + Player.inst.audioCacheMap[videoId]?.removeWhere((element) => element.file.path == path); + } + } + } + + Widget getExpansionTileWidget({ + required String title, + required String subtitle, + required IconData icon, + required List items, + required ({String title, String subtitle, String path}) Function(T item) itemBuilder, + required int Function(T item) itemSize, + }) { + return NamidaExpansionTile( + initiallyExpanded: true, + titleText: title, + subtitleText: subtitle, + icon: icon, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: context.theme.cardColor, + borderRadius: BorderRadius.circular(6.0.multipliedRadius), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 3.0), + child: Text("${items.length}"), + ), + ), + const SizedBox(width: 6.0), + const Icon(Broken.arrow_down_2, size: 20.0), + ], + ), + childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + children: items.map( + (item) { + final data = itemBuilder(item); + return SmallListTile( + borderRadius: 12.0, + color: context.theme.cardColor, + visualDensity: const VisualDensity(horizontal: -3.0, vertical: -3.0), + title: data.title, + subtitle: data.subtitle, + active: false, + onTap: () { + final wasTrue = pathsToDelete[data.path] == true; + final willEnable = !wasTrue; + pathsToDelete[data.path] = willEnable; + if (willEnable) { + totalSizeToDelete.value += itemSize(item); + } else { + totalSizeToDelete.value -= itemSize(item); + } + }, + trailing: Obx( + () => NamidaCheckMark( + size: 16.0, + active: allSelected.value || pathsToDelete[data.path] == true, + ), + ), + ); + }, + ).toList(), + ); + } + + NamidaNavigator.inst.navigateDialog( + onDisposing: () { + pathsToDelete.close(); + allSelected.close(); + totalSizeToDelete.close(); + }, + dialogBuilder: (theme) => CustomBlurryDialog( + theme: theme, + normalTitleStyle: true, + icon: Broken.trash, + title: lang.CLEAR, + trailingWidgets: [ + Obx( + () => Checkbox.adaptive( + splashRadius: 28.0, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.0.multipliedRadius), + ), + value: allSelected.value, + onChanged: (value) { + allSelected.value = !allSelected.value; + if (allSelected.value) { + totalSizeToDelete.value = audiosSize + videosSize; + } else { + int newVal = 0; + for (final k in pathsToDelete.keys) { + if (pathsToDelete[k] == true) newVal += fileSizeLookup[k] ?? 0; + } + totalSizeToDelete.value = newVal; + } + }, + ), + ), + ], + actions: [ + const CancelButton(), + Obx( + () => NamidaButton( + enabled: pathsToDelete.values.any((element) => element) || allSelected.value, + text: "${lang.DELETE} (${totalSizeToDelete.value.fileSizeFormatted})", + onPressed: () async { + if (allSelected.value) { + await Future.wait([ + deleteItems(videosCached.map((e) => e.path)), + deleteItems(audiosCached.map((e) => e.file.path)), + ]); + } else { + await deleteItems(pathsToDelete.keys.where((element) => pathsToDelete[element] == true)); + } + NamidaNavigator.inst.closeDialog(); + }, + ), + ), + ], + child: Column( + children: [ + getExpansionTileWidget( + title: lang.VIDEO_CACHE, + subtitle: videosSize.fileSizeFormatted, + icon: Broken.video, + items: videosCached, + itemSize: (item) => item.sizeInBytes, + itemBuilder: (v) { + return ( + title: "${v.resolution}p • ${v.framerate}fps ", + subtitle: v.sizeInBytes.fileSizeFormatted, + path: v.path, + ); + }, + ), + getExpansionTileWidget( + title: lang.AUDIO_CACHE, + subtitle: audiosSize.fileSizeFormatted, + icon: Broken.musicnote, + items: audiosCached, + itemSize: (item) => fileSizeLookup[item.file.path] ?? item.file.sizeInBytesSync(), + itemBuilder: (a) { + final bitrateText = a.bitrate == null ? null : "${a.bitrate! ~/ 1000}kb/s"; + final langText = a.langaugeName == null ? '' : " • ${a.langaugeName}"; + return ( + title: "${bitrateText ?? lang.AUDIO}$langText", + subtitle: a.file.fileSizeFormatted() ?? '', + path: a.file.path, + ); + }, + ), + ], + ), + ), + ); + } }