diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 7d10be52..161aaecc 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -20,6 +20,10 @@ final RegExp kYoutubeRegex = RegExp( r'\b(?:https?://)?(?:www\.)?(?:youtube\.com/watch\?v=|youtu\.be/)([\w\-]+)(?:\S+)?', caseSensitive: false, ); +final RegExp kYoutubeRegexPlaylists = RegExp( + r'\b(?:https?://)?(?:www\.)?(?:youtube\.com/playlist\?list=|youtu\.be/)([\w\-]+)(?:\S+)?', + caseSensitive: false, +); /// Main Color const Color kMainColor = Color.fromARGB(160, 117, 128, 224); diff --git a/lib/core/namida_converter_ext.dart b/lib/core/namida_converter_ext.dart index 3c400d1b..43762957 100644 --- a/lib/core/namida_converter_ext.dart +++ b/lib/core/namida_converter_ext.dart @@ -65,8 +65,10 @@ import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; import 'package:namida/youtube/functions/add_to_playlist_sheet.dart'; import 'package:namida/youtube/functions/download_sheet.dart'; +import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/pages/youtube_home_view.dart'; import 'package:namida/youtube/pages/yt_history_page.dart'; +import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/youtube_playlists_view.dart'; @@ -515,6 +517,29 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { String toText() => _NamidaConverters.inst.getTitle(this); IconData toIcon() => _NamidaConverters.inst.getIcon(this); + Future executePlaylist(String playlistUrl, {YoutubePlaylist? playlist, required BuildContext? context}) async { + final plInfo = playlist ?? await YoutubeController.inst.getPlaylistInfo(playlistUrl); + if (plInfo == null) return snackyy(title: lang.ERROR, message: 'error retrieving playlist info, check your connection?'); + final didFetch = await plInfo.fetchAllPlaylistStreams(context: context?.mounted == true ? context : null); + if (!didFetch) return snackyy(title: lang.ERROR, message: 'error fetching playlist videos'); + + final streams = plInfo.streams; + + switch (this) { + case OnYoutubeLinkOpenAction.showDownload: + plInfo.showPlaylistDownloadSheet(context: context?.mounted == true ? context : null); + case OnYoutubeLinkOpenAction.addToPlaylist: + showAddToPlaylistSheet(ids: streams.map((e) => e.id ?? ''), idsNamesLookup: {}); + case OnYoutubeLinkOpenAction.play: + await Player.inst.playOrPause(0, streams.map((e) => YoutubeID(id: e.id ?? '', playlistID: null)), QueueSource.others); + case OnYoutubeLinkOpenAction.alwaysAsk: + _showAskDialog((action) => action.executePlaylist(playlistUrl, context: context, playlist: plInfo)); + + default: + null; + } + } + Future execute(Iterable ids) async { switch (this) { case OnYoutubeLinkOpenAction.showDownload: @@ -523,50 +548,51 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { } else { NamidaNavigator.inst.navigateTo( YTPlaylistDownloadPage( - ids: ids.toList(), + ids: ids.map((e) => YoutubeID(id: e, playlistID: null)).toList(), playlistName: 'External - ${DateTime.now().millisecondsSinceEpoch.dateAndClockFormattedOriginal}', infoLookup: const {}, ), ); } case OnYoutubeLinkOpenAction.addToPlaylist: - final idnames = {for (final id in ids) id: YoutubeController.inst.getBackupVideoInfo(id)?.title ?? YoutubeController.inst.fetchVideoDetailsFromCacheSync(id)?.name}; - showAddToPlaylistSheet(ids: ids, idsNamesLookup: idnames); + showAddToPlaylistSheet(ids: ids, idsNamesLookup: {}); case OnYoutubeLinkOpenAction.play: await Player.inst.playOrPause(0, ids.map((e) => YoutubeID(id: e, playlistID: null)), QueueSource.others); case OnYoutubeLinkOpenAction.alwaysAsk: - { - final newVals = List.from(OnYoutubeLinkOpenAction.values); - newVals.remove(OnYoutubeLinkOpenAction.alwaysAsk); - NamidaNavigator.inst.navigateDialog( - dialog: CustomBlurryDialog( - title: lang.CHOOSE, - normalTitleStyle: true, - actions: [ - NamidaButton( - text: lang.DONE, - onPressed: NamidaNavigator.inst.closeDialog, - ) - ], - child: Column( - children: newVals - .map( - (e) => CustomListTile( - icon: e.toIcon(), - title: e.toText(), - onTap: () => e.execute(ids), - ), - ) - .toList(), - ), - ), - ); - } + _showAskDialog((action) => action.execute(ids)); default: null; } } + + void _showAskDialog(void Function(OnYoutubeLinkOpenAction action) onTap) { + final newVals = List.from(OnYoutubeLinkOpenAction.values); + newVals.remove(OnYoutubeLinkOpenAction.alwaysAsk); + NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + title: lang.CHOOSE, + normalTitleStyle: true, + actions: [ + NamidaButton( + text: lang.DONE, + onPressed: NamidaNavigator.inst.closeDialog, + ) + ], + child: Column( + children: newVals + .map( + (e) => CustomListTile( + icon: e.toIcon(), + title: e.toText(), + onTap: () => onTap(e), + ), + ) + .toList(), + ), + ), + ); + } } extension PerformanceModeUtils on PerformanceMode { diff --git a/lib/youtube/controller/youtube_controller.dart b/lib/youtube/controller/youtube_controller.dart index f388d089..a241b16e 100644 --- a/lib/youtube/controller/youtube_controller.dart +++ b/lib/youtube/controller/youtube_controller.dart @@ -229,13 +229,17 @@ class YoutubeController { } /// For full list of items, use [streams] getter in [playlist]. - Future> getPlaylistStreams(YoutubePlaylist? playlist) async { + Future> getPlaylistStreams(YoutubePlaylist? playlist, {bool forceInitial = false}) async { if (playlist == null) return []; - final streams = await playlist.getStreamsNextPage(); + final streams = forceInitial ? await playlist.getStreams() : await playlist.getStreamsNextPage(); _fillTempVideoInfoMap(streams); return streams; } + Future getPlaylistInfo(String playlistUrl, {bool forceInitial = false}) async { + return await NewPipeExtractorDart.playlists.getPlaylistDetails(playlistUrl); + } + Future _fetchComments(String id, {bool forceRequest = false}) async { currentTotalCommentsCount.value = null; currentComments.clear(); diff --git a/lib/youtube/functions/yt_playlist_utils.dart b/lib/youtube/functions/yt_playlist_utils.dart index 151f3d00..e46453fb 100644 --- a/lib/youtube/functions/yt_playlist_utils.dart +++ b/lib/youtube/functions/yt_playlist_utils.dart @@ -1,13 +1,20 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:newpipeextractor_dart/newpipeextractor_dart.dart' as yt; +import 'package:playlist_manager/module/playlist_id.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/navigator_controller.dart'; +import 'package:namida/core/extensions.dart'; +import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; +import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; +import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_playlist_controller.dart'; +import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; extension YoutubePlaylistShare on YoutubePlaylist { Future shareVideos() async => await tracks.shareVideos(); @@ -113,3 +120,124 @@ extension YoutubePlaylistShare on YoutubePlaylist { return controller.text; } } + +extension YoutubePlaylistHostedUtils on yt.YoutubePlaylist { + /// Sending a [context] means showing a bottom sheet with progress. + /// + /// Returns wether the fetching process ended successfully, videos are accessible through [streams] getter. + Future fetchAllPlaylistStreams({required BuildContext? context}) async { + final playlist = this; + if (playlist.streams.length == playlist.streamCount) return true; + + final currentCount = playlist.streams.length.obs; + final totalCount = playlist.streamCount.obs; + const switchAnimationDur = Duration(milliseconds: 600); + const switchAnimationDurHalf = Duration(milliseconds: 300); + + bool isTotalCountNull() => totalCount.value < 0; + + if (context != null) { + await Future.delayed(Duration.zero); + // ignore: use_build_context_synchronously + showModalBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: false, + builder: (context) { + final iconSize = context.width * 0.5; + final iconColor = context.theme.colorScheme.onBackground.withOpacity(0.6); + return SizedBox( + width: context.width, + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Obx( + () => AnimatedSwitcher( + key: const Key('circle_switch'), + duration: switchAnimationDurHalf, + child: currentCount.value < totalCount.value || isTotalCountNull() + ? ThreeArchedCircle( + size: iconSize, + color: iconColor, + ) + : Icon( + key: const Key('tick_switch'), + Broken.tick_circle, + size: iconSize, + color: iconColor, + ), + ), + ), + const SizedBox(height: 12.0), + Text( + '${lang.FETCHING}...', + style: context.textTheme.displayLarge, + ), + const SizedBox(height: 8.0), + Obx( + () => Text( + '${currentCount.value.formatDecimal()}/${isTotalCountNull() ? '?' : totalCount.value.formatDecimal()}', + style: context.textTheme.displayLarge, + ), + ), + ], + ), + ), + ); + }, + ); + } + + if (isTotalCountNull() || currentCount.value == 0) { + await YoutubeController.inst.getPlaylistStreams(playlist, forceInitial: currentCount.value == 0); + currentCount.value = playlist.streams.length; + totalCount.value = playlist.streamCount < 0 ? playlist.streams.length : playlist.streamCount; + } + + // -- if still not fetched + if (isTotalCountNull()) { + if (context != null && context.mounted) Navigator.of(context).pop(); + currentCount.close(); + return false; + } + + while (currentCount.value < totalCount.value) { + final res = await YoutubeController.inst.getPlaylistStreams(playlist); + if (res.isEmpty) break; + currentCount.value = playlist.streams.length; + } + + if (context != null) { + await Future.delayed(switchAnimationDur); + if (context.mounted) Navigator.of(context).pop(); + } + + currentCount.close(); + totalCount.close(); + return true; + } + + Future showPlaylistDownloadSheet({required BuildContext? context}) async { + final didFetch = await fetchAllPlaylistStreams(context: context); + if (!didFetch) return snackyy(title: lang.ERROR, message: 'error fetching playlist videos'); + final playlist = this; + final plID = playlist.id; + final videoIDs = playlist.streams.map((e) => YoutubeID( + id: e.id ?? '', + playlistID: plID == null ? null : PlaylistID(id: plID), + )); + final infoLookup = {}; + playlist.streams.loop((e, index) { + infoLookup[e.id ?? ''] = e; + }); + NamidaNavigator.inst.navigateTo( + YTPlaylistDownloadPage( + ids: videoIDs.toList(), + playlistName: playlist.name ?? '', + infoLookup: infoLookup, + ), + ); + } +} diff --git a/lib/youtube/pages/yt_playlist_download_subpage.dart b/lib/youtube/pages/yt_playlist_download_subpage.dart index 0dfaacd3..57239796 100644 --- a/lib/youtube/pages/yt_playlist_download_subpage.dart +++ b/lib/youtube/pages/yt_playlist_download_subpage.dart @@ -215,12 +215,12 @@ class _YTPlaylistDownloadPageState extends State { 12.0; } - double hmultiplier = 0.7; - double previousScale = 0.7; + double _hmultiplier = 0.7; + double _previousScale = 0.7; @override Widget build(BuildContext context) { - final thumWidth = context.width * 0.3 * hmultiplier; + final thumWidth = context.width * 0.3 * _hmultiplier; final thumHeight = thumWidth * 9 / 16; return BackgroundWrapper( child: Stack( @@ -286,7 +286,7 @@ class _YTPlaylistDownloadPageState extends State { slivers: [ const SliverPadding(padding: EdgeInsets.only(bottom: 12.0)), SliverFixedExtentList.builder( - itemExtent: Dimensions.youtubeCardItemExtent * hmultiplier, + itemExtent: Dimensions.youtubeCardItemExtent * _hmultiplier, itemCount: widget.ids.length, itemBuilder: (context, index) { final id = widget.ids[index].id; @@ -303,8 +303,8 @@ class _YTPlaylistDownloadPageState extends State { final fileExists = File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.value}/$filename").existsSync(); return NamidaInkWell( animationDurationMS: 200, - height: Dimensions.youtubeCardItemHeight * hmultiplier, - margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: Dimensions.youtubeCardItemVerticalPadding * hmultiplier), + height: Dimensions.youtubeCardItemHeight * _hmultiplier, + margin: EdgeInsets.symmetric(horizontal: 12.0, vertical: Dimensions.youtubeCardItemVerticalPadding * _hmultiplier), borderRadius: 12.0, bgColor: context.theme.cardColor.withOpacity(0.3), decoration: isSelected @@ -363,7 +363,7 @@ class _YTPlaylistDownloadPageState extends State { const SizedBox(height: 6.0), Text( info?.name ?? id, - style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0.multipliedFontScale * hmultiplier), + style: context.textTheme.displayMedium?.copyWith(fontSize: 15.0.multipliedFontScale * _hmultiplier), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -378,7 +378,7 @@ class _YTPlaylistDownloadPageState extends State { const SizedBox(width: 2.0), Text( info?.uploaderName ?? '', - style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale * hmultiplier), + style: context.textTheme.displaySmall?.copyWith(fontSize: 14.0.multipliedFontScale * _hmultiplier), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -512,8 +512,8 @@ class _YTPlaylistDownloadPageState extends State { ), Positioned.fill( child: GestureDetector( - onScaleStart: (details) => previousScale = hmultiplier, - onScaleUpdate: (details) => setState(() => hmultiplier = (details.scale * previousScale).clamp(0.5, 2.0)), + onScaleStart: (details) => _previousScale = _hmultiplier, + onScaleUpdate: (details) => setState(() => _hmultiplier = (details.scale * _previousScale).clamp(0.5, 2.0)), ), ), ], diff --git a/lib/youtube/widgets/yt_playlist_card.dart b/lib/youtube/widgets/yt_playlist_card.dart index 33d78f53..eeecfebb 100644 --- a/lib/youtube/widgets/yt_playlist_card.dart +++ b/lib/youtube/widgets/yt_playlist_card.dart @@ -1,22 +1,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; 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/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; -import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; -import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; +import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/widgets/yt_card.dart'; class YoutubePlaylistCard extends StatelessWidget { @@ -83,7 +80,7 @@ class YoutubePlaylistCard extends StatelessWidget { icon: Broken.import, title: lang.DOWNLOAD, onTap: () { - if (playlist != null) _showPlaylistDownloadSheet(context, playlist!); + if (playlist != null) playlist?.showPlaylistDownloadSheet(context: context); }, ), NamidaPopupItem( @@ -105,106 +102,4 @@ class YoutubePlaylistCard extends StatelessWidget { ], ); } - - Future _showPlaylistDownloadSheet(BuildContext context, YoutubePlaylist playlist) async { - final currentCount = playlist.streams.length.obs; - final totalCount = playlist.streamCount.obs; - const switchAnimationDur = Duration(milliseconds: 600); - const switchAnimationDurHalf = Duration(milliseconds: 300); - - bool isTotalCountNull() => totalCount.value < 0; - - await Future.delayed(Duration.zero); - // ignore: use_build_context_synchronously - showModalBottomSheet( - context: context, - useRootNavigator: true, - isDismissible: false, - builder: (context) { - final iconSize = context.width * 0.5; - final iconColor = context.theme.colorScheme.onBackground.withOpacity(0.6); - return SizedBox( - width: context.width, - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx( - () => AnimatedSwitcher( - key: const Key('circle_switch'), - duration: switchAnimationDurHalf, - child: currentCount.value < totalCount.value || isTotalCountNull() - ? ThreeArchedCircle( - size: iconSize, - color: iconColor, - ) - : Icon( - key: const Key('tick_switch'), - Broken.tick_circle, - size: iconSize, - color: iconColor, - ), - ), - ), - const SizedBox(height: 12.0), - Text( - '${lang.FETCHING}...', - style: context.textTheme.displayLarge, - ), - const SizedBox(height: 8.0), - Obx( - () => Text( - '${currentCount.value.formatDecimal()}/${isTotalCountNull() ? '?' : totalCount.value.formatDecimal()}', - style: context.textTheme.displayLarge, - ), - ), - ], - ), - ), - ); - }, - ); - - if (isTotalCountNull() || currentCount.value == 0) { - await YoutubeController.inst.getPlaylistStreams(playlist, forceInitial: currentCount.value == 0); - currentCount.value = playlist.streams.length; - totalCount.value = playlist.streamCount < 0 ? playlist.streams.length : playlist.streamCount; - } - - // -- if still not fetched - if (isTotalCountNull()) { - if (context.mounted) Navigator.of(context).pop(); - currentCount.close(); - return; - } - - while (currentCount.value < totalCount.value) { - final res = await YoutubeController.inst.getPlaylistStreams(playlist); - if (res.isEmpty) break; - currentCount.value = playlist.streams.length; - } - - await Future.delayed(switchAnimationDur); - if (context.mounted) Navigator.of(context).pop(); - - currentCount.close(); - - final plID = playlist.id; - final videoIDs = playlist.streams.map((e) => YoutubeID( - id: e.id ?? '', - playlistID: plID == null ? null : PlaylistID(id: plID), - )); - final infoLookup = {}; - playlist.streams.loop((e, index) { - infoLookup[e.id ?? ''] = e; - }); - NamidaNavigator.inst.navigateTo( - YTPlaylistDownloadPage( - ids: videoIDs.toList(), - playlistName: playlist.name ?? '', - infoLookup: infoLookup, - ), - ); - } } diff --git a/pubspec.yaml b/pubspec.yaml index 1151d1bc..306b0032 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,7 +107,7 @@ dependencies: newpipeextractor_dart: git: url: https://github.com/MSOB7YY/NewPipeExtractor_Dart - ref: e76397e135891f7c74ddf5a9ace2cdcebaf782d5 + ref: e54554601087e076421d290b78ea9ef058aa5ab3 # ---- Image Utilities ---- extended_image: ^8.0.2