diff --git a/lib/controller/indexer_controller.dart b/lib/controller/indexer_controller.dart index 3b0ae2b4..aa49537c 100644 --- a/lib/controller/indexer_controller.dart +++ b/lib/controller/indexer_controller.dart @@ -645,11 +645,11 @@ class Indexer { _removeThisTrackFromAlbumGenreArtistEtc(tr); }, ); - recentlyDeltedFileWrite.flush().then((_) => recentlyDeltedFileWrite.close()); SearchSortController.inst.trackSearchList.refresh(); SearchSortController.inst.trackSearchTemp.refresh(); Folders.inst.currentFolder.refresh(); await _saveTrackFileToStorage(); + recentlyDeltedFileWrite.flush().then((_) => recentlyDeltedFileWrite.close()); } Future reindexTracks({ diff --git a/lib/core/functions.dart b/lib/core/functions.dart index 09531d48..900398b6 100644 --- a/lib/core/functions.dart +++ b/lib/core/functions.dart @@ -672,24 +672,50 @@ Future showCalendarDialog({ ); } +class BottomSheetTextFieldConfig { + final String? initalControllerText; + final String hintText; + final String labelText; + final int? maxLength; + final String? Function(String? value)? validator; + + const BottomSheetTextFieldConfig({ + this.initalControllerText, + required this.hintText, + required this.labelText, + this.maxLength, + required this.validator, + }); +} + +class BottomSheetTextFieldConfigWC extends BottomSheetTextFieldConfig { + final TextEditingController controller; + + const BottomSheetTextFieldConfigWC({ + required this.controller, + required super.hintText, + required super.labelText, + super.maxLength, + required super.validator, + }) : super(initalControllerText: null); +} + Future showNamidaBottomSheetWithTextField({ required BuildContext context, bool isScrollControlled = true, bool useRootNavigator = true, bool showDragHandle = true, required String title, - String? initalControllerText, - required String hintText, - required String labelText, - int? maxLength, - required String? Function(String? value)? validator, + required BottomSheetTextFieldConfig textfieldConfig, + List? extraTextfieldsConfig, required String buttonText, TextStyle? buttonTextStyle, Color? buttonColor, required FutureOr Function(String text) onButtonTap, Widget Function(FormState formState)? extraItemsBuilder, + Rx? isInitiallyLoading, }) async { - final controller = TextEditingController(text: initalControllerText); + final localController = textfieldConfig is BottomSheetTextFieldConfigWC ? textfieldConfig.controller : TextEditingController(text: textfieldConfig.initalControllerText); final GlobalKey formKey = GlobalKey(); final focusNode = FocusNode(); @@ -706,12 +732,12 @@ Future showNamidaBottomSheetWithTextField({ isScrollControlled: isScrollControlled, builder: (context) { final bottomPadding = MediaQuery.viewInsetsOf(context).bottom + MediaQuery.paddingOf(context).bottom; - return Padding( + final child = Padding( padding: const EdgeInsets.symmetric(horizontal: 28.0).add(EdgeInsets.only(bottom: 18.0 + bottomPadding)), child: Form( key: formKey, child: NamidaLoadingSwitcher( - builder: (startLoading, stopLoading, isLoading) => Column( + builder: (loadingController) => Column( mainAxisSize: MainAxisSize.min, children: [ Text( @@ -721,11 +747,22 @@ Future showNamidaBottomSheetWithTextField({ const SizedBox(height: 18.0), CustomTagTextField( focusNode: focusNode, - controller: controller, - hintText: hintText, - labelText: labelText, - validator: validator, - maxLength: maxLength, + controller: localController, + hintText: textfieldConfig.hintText, + labelText: textfieldConfig.labelText, + validator: textfieldConfig.validator, + maxLength: textfieldConfig.maxLength, + ), + ...?extraTextfieldsConfig?.map( + (e) { + return CustomTagTextField( + controller: e.controller, + hintText: e.hintText, + labelText: e.labelText, + validator: e.validator, + maxLength: e.maxLength, + ); + }, ), if (extraItemsBuilder != null) extraItemsBuilder(formKey.currentState!), const SizedBox(height: 18.0), @@ -749,11 +786,11 @@ Future showNamidaBottomSheetWithTextField({ ), onTap: () async { if (formKey.currentState!.validate()) { - startLoading(); - final didAgree = await onButtonTap(controller.text); - stopLoading(); + loadingController.startLoading(); + final didAgree = await onButtonTap(localController.text); + loadingController.stopLoading(); if (didAgree) { - finalText = controller.text; + finalText = localController.text; if (context.mounted) context.safePop(); } } @@ -767,9 +804,37 @@ Future showNamidaBottomSheetWithTextField({ ), ), ); + return isInitiallyLoading != null + ? ObxO( + rx: isInitiallyLoading, + builder: (initiallyLoading) { + return Stack( + children: [ + AnimatedEnabled( + enabled: !initiallyLoading, + child: child, + ), + if (initiallyLoading) + const Positioned( + top: 0, + right: 32.0, + child: SizedBox( + width: 32.0, + height: 32.0, + child: CircularProgressIndicator(strokeWidth: 4.0), + ), + ), + ], + ); + }, + ) + : child; }, ); - controller.disposeAfterAnimation(also: focusNode.dispose); + Future.delayed(const Duration(milliseconds: 2000), () { + if (localController is! BottomSheetTextFieldConfigWC) localController.dispose(); + focusNode.dispose(); + }); return finalText; } diff --git a/lib/core/namida_converter_ext.dart b/lib/core/namida_converter_ext.dart index 60dd4cf9..c767768b 100644 --- a/lib/core/namida_converter_ext.dart +++ b/lib/core/namida_converter_ext.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:history_manager/history_manager.dart'; import 'package:path/path.dart' as p; -import 'package:youtipie/class/result_wrapper/playlist_result_base.dart'; import 'package:youtipie/class/streams/audio_stream.dart'; import 'package:youtipie/class/streams/video_stream.dart'; import 'package:youtipie/core/enum.dart'; @@ -58,7 +57,6 @@ import 'package:namida/youtube/functions/add_to_playlist_sheet.dart'; import 'package:namida/youtube/functions/download_sheet.dart'; import 'package:namida/youtube/pages/youtube_home_view.dart'; import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; -import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/yt_utils.dart'; extension MediaTypeUtils on MediaType { @@ -442,17 +440,7 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { } } - void _showAskDialog(void Function(OnYoutubeLinkOpenAction action) onTap, {YoutiPiePlaylistResultBase? playlistToOpen, YoutiPiePlaylistResultBase? playlistToAddAs}) { - String playlistNameToAddAs = playlistToAddAs?.basicInfo.title ?? ''; - String suffix = ''; - int suffixIndex = 1; - while (ytplc.YoutubePlaylistController.inst.playlistsMap.value["$playlistNameToAddAs$suffix"] != null) { - suffixIndex++; - suffix = ' ($suffixIndex)'; - } - playlistNameToAddAs += suffix; - - final didAddToPlaylist = false.obs; + void _showAskDialog(void Function(OnYoutubeLinkOpenAction action) onTap) { final isItemEnabled = { OnYoutubeLinkOpenAction.playNext: true, OnYoutubeLinkOpenAction.playAfter: true, @@ -463,7 +451,6 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { NamidaNavigator.inst.navigateDialog( onDisposing: () { - didAddToPlaylist.close(); isItemEnabled.close(); }, dialog: CustomBlurryDialog( @@ -474,12 +461,6 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { ], child: Column( children: [ - if (playlistToOpen != null) - CustomListTile( - icon: Broken.export_2, - title: lang.OPEN, - onTap: YTHostedPlaylistSubpage(playlist: playlistToOpen).navigate, - ), ...[ OnYoutubeLinkOpenAction.showDownload, OnYoutubeLinkOpenAction.play, @@ -510,22 +491,6 @@ extension OnYoutubeLinkOpenActionUtils on OnYoutubeLinkOpenAction { ); }, ), - if (playlistNameToAddAs != '') - Obx( - () => CustomListTile( - enabled: !didAddToPlaylist.valueR, - icon: Broken.add_square, - title: lang.ADD_AS_A_NEW_PLAYLIST, - subtitle: playlistNameToAddAs, - onTap: () { - didAddToPlaylist.value = true; - ytplc.YoutubePlaylistController.inst.addNewPlaylist( - playlistNameToAddAs, - videoIds: playlistToAddAs?.items.map((e) => e.id), - ); - }, - ), - ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index 6f136641..ad347992 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -345,7 +345,7 @@ Future _initializeIntenties() async { settings.youtube.onYoutubeLinkOpen.value.execute(youtubeIds); } else if (ytPlaylistsIds.isNotEmpty) { for (final plid in ytPlaylistsIds) { - YTHostedPlaylistSubpage.fromId(playlistId: plid).navigate(); + YTHostedPlaylistSubpage.fromId(playlistId: plid, userPlaylist: null).navigate(); } } else { final existing = paths.where((element) => File(element).existsSync()); // this for sussy links diff --git a/lib/packages/miniplayer_base.dart b/lib/packages/miniplayer_base.dart index 4d0058f2..487a8e19 100644 --- a/lib/packages/miniplayer_base.dart +++ b/lib/packages/miniplayer_base.dart @@ -1332,7 +1332,7 @@ class _TrackInfo extends StatelessWidget { final isUserLiked = currentLikeStatus == LikeStatus.liked; return NamidaLoadingSwitcher( size: 32.0, - builder: (startLoading, stopLoading, isLoading) => NamidaRawLikeButton( + builder: (loadingController) => NamidaRawLikeButton( isLiked: isUserLiked, likedIcon: textData.likedIcon, normalIcon: textData.normalIcon, @@ -1343,8 +1343,8 @@ class _TrackInfo extends StatelessWidget { page: YoutubeInfoController.current.currentVideoPage.value, isActive: isLiked, action: isLiked ? LikeAction.removeLike : LikeAction.addLike, - onStart: startLoading, - onEnd: stopLoading, + onStart: loadingController.startLoading, + onEnd: loadingController.stopLoading, ), ); }, diff --git a/lib/ui/pages/main_page.dart b/lib/ui/pages/main_page.dart index f58a1a78..a6916a91 100644 --- a/lib/ui/pages/main_page.dart +++ b/lib/ui/pages/main_page.dart @@ -257,7 +257,7 @@ class NamidaSearchBar extends StatelessWidget { try { final ytPlaylistId = NamidaLinkUtils.extractPlaylistId(val); if (ytPlaylistId != null && ytPlaylistId != '') { - YTHostedPlaylistSubpage.fromId(playlistId: ytPlaylistId).navigate(); + YTHostedPlaylistSubpage.fromId(playlistId: ytPlaylistId, userPlaylist: null).navigate(); return; } } catch (_) {} diff --git a/lib/ui/widgets/custom_widgets.dart b/lib/ui/widgets/custom_widgets.dart index e20616ff..318da16d 100644 --- a/lib/ui/widgets/custom_widgets.dart +++ b/lib/ui/widgets/custom_widgets.dart @@ -1357,8 +1357,20 @@ class NamidaWheelSlider extends StatelessWidget { } } +class NamidaLoadingController { + final void Function() startLoading; + final void Function() stopLoading; + bool isLoading; + + NamidaLoadingController({ + required this.startLoading, + required this.stopLoading, + required this.isLoading, + }); +} + class NamidaLoadingSwitcher extends StatefulWidget { - final Widget Function(void Function() startLoading, void Function() stopLoading, bool isLoading) builder; + final Widget Function(NamidaLoadingController loadingController) builder; final double? size; final bool showLoading; @@ -1374,32 +1386,37 @@ class NamidaLoadingSwitcher extends StatefulWidget { } class _NamidaLoadingSwitcherState extends State { - bool _isLoading = false; + late final loadingController = NamidaLoadingController( + startLoading: _startLoading, + stopLoading: _stopLoading, + isLoading: false, + ); void _startLoading() { - if (mounted) setState(() => _isLoading = true); + if (mounted) setState(() => loadingController.isLoading = true); } void _stopLoading() { - if (mounted) setState(() => _isLoading = false); + if (mounted) setState(() => loadingController.isLoading = false); } @override Widget build(BuildContext context) { - final child = widget.builder(_startLoading, _stopLoading, _isLoading); + final child = widget.builder(loadingController); + final isLoading = loadingController.isLoading; return Stack( fit: StackFit.loose, alignment: Alignment.center, children: [ AnimatedOpacity( - opacity: _isLoading ? 0.5 : 1.0, + opacity: isLoading ? 0.5 : 1.0, duration: const Duration(milliseconds: 200), child: child, ), - if (_isLoading && widget.showLoading) + if (isLoading && widget.showLoading) IgnorePointer( child: AnimatedOpacity( - opacity: _isLoading ? 0.8 : 0.0, + opacity: isLoading ? 0.8 : 0.0, duration: const Duration(milliseconds: 200), child: SizedBox( width: widget.size, diff --git a/lib/youtube/functions/add_to_playlist_sheet.dart b/lib/youtube/functions/add_to_playlist_sheet.dart index 427a4dfe..da5f74d7 100644 --- a/lib/youtube/functions/add_to_playlist_sheet.dart +++ b/lib/youtube/functions/add_to_playlist_sheet.dart @@ -22,6 +22,7 @@ import 'package:namida/main.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/controller/youtube_playlist_controller.dart' as pc; +import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/widgets/yt_thumbnail.dart'; import 'package:namida/youtube/youtube_playlists_view.dart'; @@ -129,10 +130,12 @@ void showAddToPlaylistSheet({ final text = await showNamidaBottomSheetWithTextField( context: context, title: lang.CONFIGURE, - initalControllerText: '', - hintText: '', - labelText: lang.NAME, - validator: (value) => pc.YoutubePlaylistController.inst.validatePlaylistName(value), + textfieldConfig: BottomSheetTextFieldConfig( + initalControllerText: '', + hintText: '', + labelText: lang.NAME, + validator: (value) => pc.YoutubePlaylistController.inst.validatePlaylistName(value), + ), buttonText: lang.ADD, onButtonTap: (text) => true, ); @@ -336,7 +339,7 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { } return NamidaLoadingSwitcher( showLoading: false, - builder: (startLoading, stopLoading, isLoading) => NamidaInkWell( + builder: (loadingController) => NamidaInkWell( animationDurationMS: 200, margin: const EdgeInsets.symmetric(vertical: 4.0), width: context.width, @@ -355,7 +358,7 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { ) : null, ), - onTap: isLoading ? null : () => _onPlaylistTap(pl, startLoading, stopLoading), + onTap: loadingController.isLoading ? null : () => _onPlaylistTap(pl, loadingController.startLoading, loadingController.stopLoading), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( @@ -400,7 +403,7 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { SizedBox( width: 22.0, height: 22.0, - child: isLoading + child: loadingController.isLoading ? const Padding( padding: EdgeInsets.all(2.0), child: CircularProgressIndicator(strokeWidth: 2.0), @@ -410,7 +413,7 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { padding: const EdgeInsets.all(2.0), child: Checkbox.adaptive( value: _newContainsVideo[pl.playlistId] ?? pl.containsVideo, - onChanged: (value) => _onPlaylistTap(pl, startLoading, stopLoading), + onChanged: (value) => _onPlaylistTap(pl, loadingController.startLoading, loadingController.stopLoading), ), ) : _newContainsVideo[pl.playlistId] == true @@ -514,77 +517,17 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { ], ), onTap: () async { - final privacyIconsLookup = { - PlaylistPrivacy.public: Broken.global, - PlaylistPrivacy.unlisted: Broken.link, - PlaylistPrivacy.private: Broken.lock_1, - }; - final privacyRx = PlaylistPrivacy.private.obs; const addAsInitial = true; - await showNamidaBottomSheetWithTextField( + YtUtilsPlaylist().promptCreatePlaylist( context: context, - title: lang.CONFIGURE, - initalControllerText: '', - hintText: '', - maxLength: YoutubeInfoController.userplaylist.MAX_PLAYLIST_NAME, - labelText: lang.NAME, - extraItemsBuilder: (formState) => Column( - children: [ - const SizedBox(height: 12.0), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ObxO( - rx: privacyRx, - builder: (privacy) => Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: PlaylistPrivacy.values.map( - (e) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: NamidaInkWellButton( - icon: privacyIconsLookup[e], - text: e.toText(), - bgColor: context.theme.colorScheme.secondaryContainer.withOpacity(privacy == e ? 0.5 : 0.2), - onTap: () => privacyRx.value = e, - trailing: const SizedBox( - width: 16.0, - height: 16.0, - child: Checkbox.adaptive( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(6.0)), - ), - value: true, - onChanged: null, - ), - ).animateEntrance( - showWhen: privacy == e, - allCurves: Curves.fastLinearToSlowEaseIn, - durationMS: 300, - ), - ), - ); - }, - ).toList(), - ), - ), - ), - ], - ), - validator: (value) { - if (value == null || value.isEmpty) return lang.PLEASE_ENTER_A_NAME; - return YoutubeInfoController.userplaylist.validatePlaylistTitle(value); - }, - buttonText: lang.ADD, - onButtonTap: (text) async { - if (text.isEmpty) return false; - - final playlistTitle = text; + onButtonConfirm: (playlistTitle, privacy) async { + privacy ??= PlaylistPrivacy.private; final newPlaylistId = await YoutubeInfoController.userplaylist.createPlaylist( + mainList: YtUtilsPlaylist.activeUserPlaylistsList, title: playlistTitle, // ignore: dead_code initialVideoIds: addAsInitial ? videosList : [], - privacy: privacyRx.value, + privacy: privacy, ); if (newPlaylistId != null) { if (mounted) { @@ -594,7 +537,7 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { PlaylistForVideoItem( playlistId: newPlaylistId, playlistTitle: playlistTitle, - privacy: privacyRx.value, + privacy: privacy, containsVideo: addAsInitial, ), ); @@ -606,9 +549,6 @@ class __PlaylistsForVideoPageState extends State<_PlaylistsForVideoPage> { return false; }, ); - Future.delayed(const Duration(microseconds: 300), () { - privacyRx.close(); - }); }, ), ), diff --git a/lib/youtube/functions/yt_playlist_utils.dart b/lib/youtube/functions/yt_playlist_utils.dart index a99ea2e6..bcac473e 100644 --- a/lib/youtube/functions/yt_playlist_utils.dart +++ b/lib/youtube/functions/yt_playlist_utils.dart @@ -1,13 +1,20 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:nampack/core/main_utils.dart'; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:share_plus/share_plus.dart'; import 'package:youtipie/class/result_wrapper/list_wrapper_base.dart'; +import 'package:youtipie/class/result_wrapper/playlist_mix_result.dart'; import 'package:youtipie/class/result_wrapper/playlist_result.dart'; import 'package:youtipie/class/result_wrapper/playlist_result_base.dart'; +import 'package:youtipie/class/result_wrapper/playlist_user_result.dart'; import 'package:youtipie/class/stream_info_item/stream_info_item.dart'; import 'package:youtipie/class/youtipie_feed/playlist_basic_info.dart'; +import 'package:youtipie/class/youtipie_feed/playlist_info_item_user.dart'; +import 'package:youtipie/core/enum.dart'; +import 'package:youtipie/youtipie.dart'; import 'package:namida/class/route.dart'; import 'package:namida/controller/navigator_controller.dart'; @@ -16,11 +23,13 @@ import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/functions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.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_account_controller.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/controller/youtube_playlist_controller.dart'; import 'package:namida/youtube/functions/add_to_playlist_sheet.dart'; @@ -28,6 +37,172 @@ import 'package:namida/youtube/pages/yt_playlist_download_subpage.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/yt_utils.dart'; +class YtUtilsPlaylist { + static Rxn? activeUserPlaylistsList; + static final activePlaylists = []; + + Future promptCreatePlaylist({ + required BuildContext context, + required FutureOr Function(String title, PlaylistPrivacy? privacy) onButtonConfirm, + }) => + _promptCreateOrEditPlaylist( + context: context, + isEdit: false, + playlistId: null, + initialTitle: null, + initialDescription: null, + initialPrivacy: null, + onButtonConfirm: (text, _, privacy) => onButtonConfirm(text, privacy), + ); + + Future promptEditPlaylist({ + required BuildContext context, + required YoutiPiePlaylistResult playlist, + required PlaylistInfoItemUser userPlaylist, + required FutureOr Function(String title, String? description, PlaylistPrivacy? privacy) onButtonConfirm, + }) => + _promptCreateOrEditPlaylist( + context: context, + isEdit: true, + playlistId: playlist.info.id.isNotEmpty ? playlist.info.id : userPlaylist.id, + initialTitle: playlist.info.title.isNotEmpty ? playlist.info.title : userPlaylist.title, + initialDescription: playlist.info.description, + initialPrivacy: playlist.info.privacy ?? userPlaylist.privacy, + onButtonConfirm: onButtonConfirm, + ); + + Future _promptCreateOrEditPlaylist({ + required BuildContext context, + required bool isEdit, + required String? playlistId, + required String? initialTitle, + required String? initialDescription, + required PlaylistPrivacy? initialPrivacy, + required FutureOr Function(String title, String? description, PlaylistPrivacy? privacy) onButtonConfirm, + }) async { + final privacyIconsLookup = { + PlaylistPrivacy.public: Broken.global, + PlaylistPrivacy.unlisted: Broken.link, + PlaylistPrivacy.private: Broken.lock_1, + }; + final privacyRx = (isEdit ? initialPrivacy : PlaylistPrivacy.private).obs; + final titleController = TextEditingController(text: initialTitle); + final descriptionController = isEdit ? TextEditingController(text: initialDescription) : null; + + Rx? isInitiallyLoading; + // final shouldLoadEditInfo = isEdit && playlistId != null && (initialTitle == null || initialTitle.isEmpty || initialPrivacy == null); + final shouldLoadEditInfo = isEdit && playlistId != null; // we prefer always loading live info for better cross-device sync + + if (shouldLoadEditInfo) { + isInitiallyLoading = true.obs; + isInitiallyLoading.value = true; + Future fillEditInfo() async { + try { + final plEditInfo = await YoutubeInfoController.userplaylist.getPlaylistEditInfo(playlistId); + if (plEditInfo != null) { + try { + titleController.text = plEditInfo.title; + } catch (_) {} + try { + if (plEditInfo.description != null) descriptionController?.text = plEditInfo.description!; + } catch (_) {} + if (plEditInfo.privacy != null) privacyRx.value = plEditInfo.privacy!; + } + } catch (_) {} + + isInitiallyLoading?.value = false; + } + + fillEditInfo(); + } + + await showNamidaBottomSheetWithTextField( + context: context, + title: lang.CONFIGURE, + isInitiallyLoading: isInitiallyLoading, + textfieldConfig: BottomSheetTextFieldConfigWC( + controller: titleController, + hintText: initialTitle ?? '', + maxLength: YoutubeInfoController.userplaylist.MAX_PLAYLIST_NAME, + labelText: lang.NAME, + validator: (value) { + if (value == null || value.isEmpty) return lang.PLEASE_ENTER_A_NAME; + return YoutubeInfoController.userplaylist.validatePlaylistTitle(value); + }, + ), + extraTextfieldsConfig: descriptionController == null + ? null + : [ + BottomSheetTextFieldConfigWC( + controller: descriptionController, + hintText: initialDescription ?? '', + maxLength: YoutubeInfoController.userplaylist.MAX_PLAYLIST_DESCRIPTION, + labelText: lang.DESCRIPTION, + validator: (value) => null, + ), + ], + extraItemsBuilder: (formState) => Column( + children: [ + const SizedBox(height: 12.0), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ObxO( + rx: privacyRx, + builder: (privacy) => Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: PlaylistPrivacy.values.map( + (e) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: NamidaInkWellButton( + icon: privacyIconsLookup[e], + text: e.toText(), + bgColor: context.theme.colorScheme.secondaryContainer.withOpacity(privacy == e ? 0.5 : 0.2), + onTap: () => privacyRx.value = e, + trailing: const SizedBox( + width: 16.0, + height: 16.0, + child: Checkbox.adaptive( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6.0)), + ), + value: true, + onChanged: null, + ), + ).animateEntrance( + showWhen: privacy == e, + allCurves: Curves.fastLinearToSlowEaseIn, + durationMS: 300, + ), + ), + ); + }, + ).toList(), + ), + ), + ), + ], + ), + buttonText: isEdit ? lang.SAVE : lang.ADD, + onButtonTap: (title) async { + if (title.isEmpty) return false; + final description = descriptionController?.text; + return onButtonConfirm(title, description, privacyRx.value); + }, + ); + Future.delayed(const Duration(milliseconds: 2000), () { + privacyRx.close(); + try { + titleController.dispose(); + } catch (_) {} + try { + descriptionController?.dispose(); + } catch (_) {} + }); + } +} + extension YoutubePlaylistShare on YoutubePlaylist { Future shareVideos() async => await tracks.shareVideos(); @@ -61,11 +236,13 @@ extension YoutubePlaylistShare on YoutubePlaylist { }) async { return await showNamidaBottomSheetWithTextField( context: context, - initalControllerText: playlistName, title: lang.RENAME_PLAYLIST, - hintText: playlistName, - labelText: lang.NAME, - validator: (value) => YoutubePlaylistController.inst.validatePlaylistName(value), + textfieldConfig: BottomSheetTextFieldConfig( + initalControllerText: playlistName, + hintText: playlistName, + labelText: lang.NAME, + validator: (value) => YoutubePlaylistController.inst.validatePlaylistName(value), + ), buttonText: lang.SAVE, onButtonTap: (text) async { final didRename = await YoutubePlaylistController.inst.renamePlaylist(playlistName, text); @@ -234,8 +411,10 @@ extension PlaylistBasicInfoExt on PlaylistBasicInfo { } List getPopupMenuItems({ + required BuildContext context, required bool showProgressSheet, required YoutiPiePlaylistResultBase playlistToFetch, + required PlaylistInfoItemUser? userPlaylist, bool displayDownloadItem = true, bool displayShuffle = true, bool displayPlay = true, @@ -243,17 +422,34 @@ extension PlaylistBasicInfoExt on PlaylistBasicInfo { }) { final playlist = this; final videosCount = playlist.videosCount; - final countText = videosCount == null || videosCount < 0 ? "+25" : videosCount.formatDecimalShort(); + String? countText; + if (playlistToFetch is YoutiPieMixPlaylistResult) { + countText = videosCount?.formatDecimalShort() ?? '+25'; + } else if (playlistToFetch is YoutiPiePlaylistResult) { + countText = videosCount?.formatDecimalShort(); + } + countText ??= '?'; + final playAfterVid = YTUtils.getPlayerAfterVideo(); Future> fetchAllIDs() async => await fetchAllPlaylistAsYTIDs(showProgressSheet: showProgressSheet, playlistToFetch: playlistToFetch); + String playlistNameToAddAs = playlistToFetch.basicInfo.title; + String suffix = ''; + int suffixIndex = 1; + while (YoutubePlaylistController.inst.playlistsMap.value["$playlistNameToAddAs$suffix"] != null) { + suffixIndex++; + suffix = ' ($suffixIndex)'; + } + playlistNameToAddAs += suffix; + final isInYTOnlineLibrary = playlistToFetch is YoutiPiePlaylistResult ? playlistToFetch.info.isInLibrary.value : null; return [ if (playlistToFetch is YoutiPiePlaylistResult && isInYTOnlineLibrary != null) NamidaPopupItem( icon: Broken.archive, title: isInYTOnlineLibrary ? lang.REMOVE_FROM_LIBRARY : lang.SAVE_TO_LIBRARY, + trailing: const Icon(Broken.global, size: 14.0), onTap: () async { bool? didSuccess; if (isInYTOnlineLibrary) { @@ -272,6 +468,23 @@ extension PlaylistBasicInfoExt on PlaylistBasicInfo { } }, ), + if (playlistNameToAddAs != '') + NamidaPopupItem( + icon: Broken.add_square, + title: lang.ADD_AS_A_NEW_PLAYLIST, + subtitle: playlistNameToAddAs, + onTap: () async { + final didFetch = await playlist.fetchAllPlaylistStreams(showProgressSheet: showProgressSheet, playlist: playlistToFetch); + if (!didFetch) { + snackyy(title: lang.ERROR, message: 'error fetching playlist videos'); + return; + } + YoutubePlaylistController.inst.addNewPlaylist( + playlistNameToAddAs, + videoIds: playlistToFetch.items.map((e) => e.id), + ); + }, + ), NamidaPopupItem( icon: Broken.music_playlist, title: lang.ADD_TO_PLAYLIST, @@ -296,6 +509,32 @@ extension PlaylistBasicInfoExt on PlaylistBasicInfo { ); }, ), + if (userPlaylist != null && + playlistToFetch is YoutiPiePlaylistResult && + (playlistToFetch.info.id.length == 34 || playlistToFetch.info.id.length == 36) && // exludes mixes & defaults (WL & LL) + playlistToFetch.info.uploader?.id == YoutubeAccountController.current.activeAccountChannel.value?.id) + NamidaPopupItem( + icon: Broken.edit_2, + title: lang.EDIT, + onTap: () async { + YtUtilsPlaylist().promptEditPlaylist( + context: context, + playlist: playlistToFetch, + userPlaylist: userPlaylist, + onButtonConfirm: (playlistTitle, description, privacy) async { + final didEdit = await YoutubeInfoController.userplaylist.editPlaylist( + mainList: YtUtilsPlaylist.activeUserPlaylistsList, + playlists: YtUtilsPlaylist.activePlaylists, + playlistId: playlistToFetch.info.id, + title: playlistTitle, + description: description, + privacy: privacy, + ); + return didEdit == true; + }, + ); + }, + ), NamidaPopupItem( icon: Broken.share, title: lang.SHARE, @@ -317,7 +556,10 @@ extension PlaylistBasicInfoExt on PlaylistBasicInfo { NamidaPopupItem( icon: Broken.export_2, title: lang.OPEN, - onTap: YTHostedPlaylistSubpage(playlist: playlistToFetch).navigate, + onTap: YTHostedPlaylistSubpage( + playlist: playlistToFetch, + userPlaylist: userPlaylist, + ).navigate, ), if (displayPlay) NamidaPopupItem( diff --git a/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart b/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart index c9260322..b7a7fd3b 100644 --- a/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart +++ b/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart @@ -37,6 +37,7 @@ class YoutubeMainPageFetcherAccBase, T extends final bool showRefreshInsteadOfRefreshing; final Widget? pageHeader; + final Widget? headerTrailing; final void Function()? onHeaderTap; final bool isHorizontal; final double? horizontalHeight; @@ -45,6 +46,8 @@ class YoutubeMainPageFetcherAccBase, T extends final Future Function()? onPullToRefresh; final bool enablePullToRefresh; final void Function(W? result)? onListUpdated; + final void Function(Rxn wrapper)? onInitState; + final void Function(Rxn wrapper)? onDispose; const YoutubeMainPageFetcherAccBase({ super.key, @@ -58,6 +61,7 @@ class YoutubeMainPageFetcherAccBase, T extends this.sliverListBuilder, this.showRefreshInsteadOfRefreshing = false, this.pageHeader, + this.headerTrailing, this.onHeaderTap, this.isHorizontal = false, this.horizontalHeight, @@ -66,6 +70,8 @@ class YoutubeMainPageFetcherAccBase, T extends this.onPullToRefresh, this.enablePullToRefresh = true, this.onListUpdated, + this.onInitState, + this.onDispose, }); @override @@ -80,7 +86,7 @@ class _YoutubePageState, T extends MapSerializa @override double get maxDistance => 64.0; - Future forceFetchFeed() => _fetchFeed(); + Future forceFetchFeed() => _fetchFeedSilent(); void updateList(W? list) { _currentFeed.value = list; _lastFetchWasCached.value = false; @@ -154,6 +160,7 @@ class _YoutubePageState, T extends MapSerializa @override void initState() { super.initState(); + widget.onInitState?.call(_currentFeed); _onInit(); YoutubeAccountController.current.addOnAccountChanged(_onAccChanged); if (widget.onListUpdated != null) _currentFeed.addListener(_onListUpdated); @@ -161,6 +168,7 @@ class _YoutubePageState, T extends MapSerializa @override void dispose() { + widget.onDispose?.call(_currentFeed); if (widget.onListUpdated != null) _currentFeed.removeListener(_onListUpdated); YoutubeAccountController.current.removeOnAccountChanged(_onAccChanged); @@ -235,6 +243,7 @@ class _YoutubePageState, T extends MapSerializa ) : const SizedBox(), ), + if (widget.headerTrailing != null) widget.headerTrailing!, if (widget.onHeaderTap != null) const SizedBox(width: 12.0), if (widget.onHeaderTap != null) const Icon(Broken.arrow_right_3), ], diff --git a/lib/youtube/pages/youtube_user_playlists_page.dart b/lib/youtube/pages/youtube_user_playlists_page.dart index 6db23dad..9460b84e 100644 --- a/lib/youtube/pages/youtube_user_playlists_page.dart +++ b/lib/youtube/pages/youtube_user_playlists_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:youtipie/class/result_wrapper/playlist_user_result.dart'; import 'package:youtipie/class/youtipie_feed/playlist_info_item_user.dart'; +import 'package:youtipie/core/enum.dart'; import 'package:youtipie/youtipie.dart'; import 'package:namida/class/route.dart'; @@ -9,8 +10,10 @@ import 'package:namida/core/dimensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/controller/youtube_account_controller.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; +import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/pages/user/youtube_account_manage_page.dart'; import 'package:namida/youtube/pages/youtube_main_page_fetcher_acc_base.dart'; import 'package:namida/youtube/pages/youtube_user_history_page.dart'; @@ -39,8 +42,34 @@ class YoutubeUserPlaylistsPage extends StatelessWidget { horizontalHistory, ], ), + onInitState: (wrapper) { + YtUtilsPlaylist.activeUserPlaylistsList = wrapper; + }, + onDispose: (wrapper) { + YtUtilsPlaylist.activeUserPlaylistsList = null; + }, onPullToRefresh: () => (horizontalHistoryKey.currentState as dynamic)?.forceFetchFeed() as Future, title: lang.PLAYLISTS, + headerTrailing: NamidaIconButton( + icon: Broken.add_circle, + iconSize: 22.0, + onPressed: () { + YtUtilsPlaylist().promptCreatePlaylist( + context: context, + onButtonConfirm: (playlistTitle, privacy) async { + privacy ??= PlaylistPrivacy.private; + final newPlaylistId = await YoutubeInfoController.userplaylist.createPlaylist( + mainList: YtUtilsPlaylist.activeUserPlaylistsList, + title: playlistTitle, + initialVideoIds: [], + privacy: privacy, + ); + if (newPlaylistId != null) return true; + return false; + }, + ); + }, + ), cacheReader: YoutiPie.cacheBuilder.forUserPlaylists(), networkFetcher: (details) => YoutubeInfoController.userplaylist.getUserPlaylists(details: details), itemExtent: thumbnailItemExtent, @@ -53,11 +82,11 @@ class YoutubeUserPlaylistsPage extends StatelessWidget { return YoutubePlaylistCard( key: Key(playlist.id), playlist: playlist, - subtitle: playlist.infoTexts?.firstOrNull, // the second text is mostly like 'updated today' etc + subtitle: playlist.infoTexts?.join(' - '), // the second text is mostly like 'updated today' etc thumbnailWidth: thumbnailWidth, thumbnailHeight: thumbnailHeight, firstVideoID: null, - isMixPlaylist: false, // TODO: is it possible? + isMixPlaylist: playlist.isMix, playingId: null, playOnTap: false, ); diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart index 4dbb010f..39eba751 100644 --- a/lib/youtube/pages/yt_playlist_subpage.dart +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -9,6 +9,7 @@ import 'package:youtipie/class/result_wrapper/playlist_result.dart'; import 'package:youtipie/class/result_wrapper/playlist_result_base.dart'; import 'package:youtipie/class/stream_info_item/stream_info_item.dart'; import 'package:youtipie/class/youtipie_feed/playlist_basic_info.dart'; +import 'package:youtipie/class/youtipie_feed/playlist_info_item_user.dart'; import 'package:youtipie/youtipie.dart'; import 'package:namida/base/pull_to_refresh.dart'; @@ -366,16 +367,19 @@ class YTHostedPlaylistSubpage extends StatefulWidget with NamidaRouteWidget { RouteType get route => RouteType.YOUTUBE_PLAYLIST_SUBPAGE_HOSTED; final YoutiPiePlaylistResultBase playlist; + final PlaylistInfoItemUser? userPlaylist; final bool isMixPlaylist; const YTHostedPlaylistSubpage({ super.key, required this.playlist, + required this.userPlaylist, }) : isMixPlaylist = playlist is YoutiPieMixPlaylistResult; YTHostedPlaylistSubpage.fromId({ super.key, required String playlistId, + required this.userPlaylist, }) : playlist = _EmptyPlaylistResult(playlistId: playlistId), isMixPlaylist = playlistId.startsWith('RD') && playlistId.length == 13; @@ -414,6 +418,14 @@ class _YTHostedPlaylistSubpageState extends State with YoutiPieFetchAllRes? _currentFetchAllRes; late YoutiPiePlaylistResultBase _playlist; + + late final YoutiPiePlaylistEditCallbacks _playlistInfoEditUpdater = YoutiPiePlaylistEditCallbacks( + oldPlaylist: () => _playlist, + newPlaylistCallback: (newPlaylist) { + refreshState(() => _playlist = newPlaylist); + }, + ); + @override void initState() { _playlist = widget.playlist; @@ -421,13 +433,22 @@ class _YTHostedPlaylistSubpageState extends State with sorting.value = null; // we eventually need to implement playlist sort if account is signed in. super.initState(); + final cached = YoutiPie.cacheBuilder.forPlaylistVideos(playlistId: _playlist.basicInfo.id).read(); + if (cached != null) { + _playlist = cached; + refreshState(trySortStreams); + } + onRefresh(() => _fetch100Video(forceRequest: _playlist is YoutiPiePlaylistResult), forceProceed: true); + + YtUtilsPlaylist.activePlaylists.add(_playlistInfoEditUpdater); } @override void dispose() { _isLoadingMoreItems.close(); disposeResources(); + YtUtilsPlaylist.activePlaylists.remove(_playlistInfoEditUpdater); super.dispose(); } @@ -491,7 +512,6 @@ class _YTHostedPlaylistSubpageState extends State with const itemsThumbnailWidth = Dimensions.youtubeThumbnailWidth; const itemsThumbnailItemExtent = itemsThumbnailHeight + 8.0 * 2; - final videosCount = playlist.basicInfo.videosCount; String? description; String uploaderTitleAndViews = ''; String? thumbnailUrl; @@ -509,6 +529,16 @@ class _YTHostedPlaylistSubpageState extends State with thumbnailUrl = playlist.info.thumbnails.pick()?.url; plId = playlist.playlistId; } + + String? videosCountTextFinal; + final videosCount = playlist.basicInfo.videosCount; + if (playlist is YoutiPieMixPlaylistResult) { + videosCountTextFinal = videosCount?.displayVideoKeyword; + } else if (playlist is YoutiPiePlaylistResult) { + videosCountTextFinal = videosCount?.displayVideoKeyword; + } + videosCountTextFinal ??= playlist.basicInfo.videosCountText ?? '?'; + plId ??= playlist.basicInfo.id; final plIdWrapper = PlaylistID(id: plId); final firstID = playlist.items.firstOrNull?.id; @@ -592,7 +622,7 @@ class _YTHostedPlaylistSubpageState extends State with ), const SizedBox(height: 6.0), Text( - videosCount == null ? '+25' : videosCount.displayVideoKeyword, + videosCountTextFinal ?? '', style: context.textTheme.displaySmall, ), if (uploaderTitleAndViews.isNotEmpty == true) ...[ @@ -645,7 +675,9 @@ class _YTHostedPlaylistSubpageState extends State with NamidaPopupWrapper( openOnLongPress: false, childrenDefault: () => playlist.basicInfo.getPopupMenuItems( + context: context, playlistToFetch: _playlist, + userPlaylist: widget.userPlaylist, showProgressSheet: true, displayDownloadItem: false, displayShuffle: false, diff --git a/lib/youtube/widgets/yt_comment_card.dart b/lib/youtube/widgets/yt_comment_card.dart index 6bba0b87..dd1bd995 100644 --- a/lib/youtube/widgets/yt_comment_card.dart +++ b/lib/youtube/widgets/yt_comment_card.dart @@ -294,7 +294,7 @@ class _YTCommentCardState extends State { if (comment != null) NamidaLoadingSwitcher( size: 16.0, - builder: (startLoading, stopLoading, isLoading) => NamidaRawLikeButton( + builder: (loadingController) => NamidaRawLikeButton( isLiked: currentLikeStatus == LikeStatus.liked, likedIcon: Broken.like_filled, normalIcon: Broken.like_1, @@ -303,8 +303,8 @@ class _YTCommentCardState extends State { return _onChangeLikeStatus( isLiked, isLiked ? LikeAction.removeLike : LikeAction.addLike, - startLoading, - stopLoading, + loadingController.startLoading, + loadingController.stopLoading, ); }, ), @@ -326,7 +326,7 @@ class _YTCommentCardState extends State { if (comment != null) NamidaLoadingSwitcher( size: 16.0, - builder: (startLoading, stopLoading, isLoading) => NamidaRawLikeButton( + builder: (loadingController) => NamidaRawLikeButton( isLiked: currentLikeStatus == LikeStatus.disliked, likedIcon: Broken.dislike_filled, normalIcon: Broken.dislike, @@ -335,8 +335,8 @@ class _YTCommentCardState extends State { return _onChangeLikeStatus( isDisLiked, isDisLiked ? LikeAction.removeDislike : LikeAction.addDislike, - startLoading, - stopLoading, + loadingController.startLoading, + loadingController.stopLoading, ); }, ), diff --git a/lib/youtube/widgets/yt_description_widget.dart b/lib/youtube/widgets/yt_description_widget.dart index d76b2020..48b25135 100644 --- a/lib/youtube/widgets/yt_description_widget.dart +++ b/lib/youtube/widgets/yt_description_widget.dart @@ -120,7 +120,7 @@ class YoutubeDescriptionWidgetManager { } else if (sw.channelId != null) { onTap = YTChannelSubpage(channelID: sw.channelId!, channel: null).navigate; } else if (sw.playlistId != null) { - onTap = YTHostedPlaylistSubpage.fromId(playlistId: sw.playlistId!).navigate; + onTap = YTHostedPlaylistSubpage.fromId(playlistId: sw.playlistId!, userPlaylist: null).navigate; } else if (sw.link != null) { onTap = () => NamidaLinkUtils.openLink(sw.link!); } diff --git a/lib/youtube/widgets/yt_download_task_item_card.dart b/lib/youtube/widgets/yt_download_task_item_card.dart index f2e83cb9..14a478d9 100644 --- a/lib/youtube/widgets/yt_download_task_item_card.dart +++ b/lib/youtube/widgets/yt_download_task_item_card.dart @@ -430,24 +430,26 @@ class YTDownloadTaskItemCard extends StatelessWidget { }) async { await showNamidaBottomSheetWithTextField( context: context, - initalControllerText: config.filename.filename, title: lang.RENAME, - hintText: config.filename.filename, - labelText: lang.FILE_NAME, - validator: (value) { - if (value == null || value.isEmpty) return lang.EMPTY_VALUE; - - if (value.startsWith('.')) return "${lang.FILENAME_SHOULDNT_START_WITH} ."; - - final filenameClean = YoutubeController.inst.cleanupFilename(value); - if (value != filenameClean) { - const baddiesAll = YoutubeController.cleanupFilenameRegex; // should remove \ but whatever - final baddies = baddiesAll.split('').where((element) => value.contains(element)).join(); - return "${lang.NAME_CONTAINS_BAD_CHARACTER} $baddies"; - } - - return null; - }, + textfieldConfig: BottomSheetTextFieldConfig( + initalControllerText: config.filename.filename, + hintText: config.filename.filename, + labelText: lang.FILE_NAME, + validator: (value) { + if (value == null || value.isEmpty) return lang.EMPTY_VALUE; + + if (value.startsWith('.')) return "${lang.FILENAME_SHOULDNT_START_WITH} ."; + + final filenameClean = YoutubeController.inst.cleanupFilename(value); + if (value != filenameClean) { + const baddiesAll = YoutubeController.cleanupFilenameRegex; // should remove \ but whatever + final baddies = baddiesAll.split('').where((element) => value.contains(element)).join(); + return "${lang.NAME_CONTAINS_BAD_CHARACTER} $baddies"; + } + + return null; + }, + ), buttonText: lang.SAVE, onButtonTap: (text) async { final wasDownloading = YoutubeController.inst.isDownloading[config.id]?[config.filename] ?? false; diff --git a/lib/youtube/widgets/yt_playlist_card.dart b/lib/youtube/widgets/yt_playlist_card.dart index 13426736..cae890cb 100644 --- a/lib/youtube/widgets/yt_playlist_card.dart +++ b/lib/youtube/widgets/yt_playlist_card.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:youtipie/class/execute_details.dart'; import 'package:youtipie/class/result_wrapper/playlist_result_base.dart'; import 'package:youtipie/class/youtipie_feed/playlist_basic_info.dart'; +import 'package:youtipie/class/youtipie_feed/playlist_info_item_user.dart'; import 'package:youtipie/core/enum.dart'; import 'package:youtipie/youtipie.dart'; @@ -58,8 +59,8 @@ class _YoutubePlaylistCardState extends State { Future _fetchFunction({required bool forceRequest}) async { final executeDetails = forceRequest ? ExecuteDetails.forceRequest() : ExecuteDetails.cache(CacheDecision.cacheOnly); if (widget.isMixPlaylist) { - final videoId = widget.firstVideoID ?? widget.playingId?.call(); - if (videoId == null) return null; + final videoId = widget.firstVideoID ?? widget.playingId?.call() ?? widget.playlist.id.substring(2); + if (videoId.isEmpty) return null; return YoutubeInfoController.playlist.getMixPlaylist( videoId: videoId, details: executeDetails, @@ -96,7 +97,7 @@ class _YoutubePlaylistCardState extends State { @override void initState() { super.initState(); - _fetchInitial(); + _fetchTimer = Timer(const Duration(seconds: 1), _fetchInitial); } @override @@ -105,13 +106,20 @@ class _YoutubePlaylistCardState extends State { super.dispose(); } + PlaylistInfoItemUser? get _userPlaylist { + final pl = widget.playlist; + return pl is PlaylistInfoItemUser ? pl : null; + } + List getMenuItems() { if (_fetchTimer?.isActive == true || this.playlistToFetch == null) _forceFetch(); final playlistToFetch = this.playlistToFetch; if (playlistToFetch == null) return []; return widget.playlist.getPopupMenuItems( + context: context, playlistToFetch: playlistToFetch, + userPlaylist: _userPlaylist, showProgressSheet: true, displayPlay: !widget.playOnTap, displayOpenPlaylist: widget.playOnTap, @@ -123,9 +131,9 @@ class _YoutubePlaylistCardState extends State { final playlist = widget.playlist; String countText; if (widget.isMixPlaylist) { - countText = '+25'; + countText = playlist.videosCount?.formatDecimalShort() ?? '+25'; } else { - countText = playlist.videosCount?.formatDecimalShort() ?? '?'; + countText = playlist.videosCount?.formatDecimalShort() ?? (widget.playlist.videosCountText == 'No videos' ? '0' : '?'); } final thumbnailUrl = playlist.thumbnails.pick()?.url; final firstVideoID = widget.firstVideoID; @@ -148,16 +156,24 @@ class _YoutubePlaylistCardState extends State { subtitle: widget.subtitle ?? '', thirdLineText: '', onTap: () async { - if (_fetchTimer?.isActive == true || this.playlistToFetch == null) _forceFetch(); - final playlistToFetch = this.playlistToFetch; - if (playlistToFetch == null) return; if (widget.playOnTap) { - final videos = await playlist.fetchAllPlaylistAsYTIDs(showProgressSheet: true, playlistToFetch: playlistToFetch); - if (videos.isEmpty) return; - Player.inst.playOrPause(0, videos, QueueSource.others); + if (_fetchTimer?.isActive == true || this.playlistToFetch == null) _forceFetch(); + if (playlistToFetch != null) { + final videos = await playlist.fetchAllPlaylistAsYTIDs(showProgressSheet: true, playlistToFetch: playlistToFetch); + if (videos.isEmpty) return; + Player.inst.playOrPause(0, videos, QueueSource.others); + } } else { - YTHostedPlaylistSubpage(playlist: playlistToFetch).navigate(); + playlistToFetch != null + ? YTHostedPlaylistSubpage( + playlist: playlistToFetch, + userPlaylist: _userPlaylist, + ).navigate() + : YTHostedPlaylistSubpage.fromId( + playlistId: playlist.id, + userPlaylist: _userPlaylist, + ).navigate(); } }, displayChannelThumbnail: false, diff --git a/lib/youtube/widgets/yt_subscribe_buttons.dart b/lib/youtube/widgets/yt_subscribe_buttons.dart index 48f75a07..d9a8b1b6 100644 --- a/lib/youtube/widgets/yt_subscribe_buttons.dart +++ b/lib/youtube/widgets/yt_subscribe_buttons.dart @@ -254,15 +254,17 @@ class _YTSubscribeButtonState extends State { Future _onAddGroupTap({required bool Function(String text) doesNameExist, required void Function(String text) onAdd}) async { final text = await showNamidaBottomSheetWithTextField( context: context, - initalControllerText: '', title: '', - hintText: '', - labelText: lang.GROUP, - validator: (value) { - if (value == null || value.isEmpty) return lang.EMPTY_VALUE; - if (doesNameExist(value)) return lang.PLEASE_ENTER_A_DIFFERENT_NAME; - return null; - }, + textfieldConfig: BottomSheetTextFieldConfig( + initalControllerText: '', + hintText: '', + labelText: lang.GROUP, + validator: (value) { + if (value == null || value.isEmpty) return lang.EMPTY_VALUE; + if (doesNameExist(value)) return lang.PLEASE_ENTER_A_DIFFERENT_NAME; + return null; + }, + ), buttonText: lang.ADD, onButtonTap: (text) => true, ); @@ -442,22 +444,19 @@ class _YTSubscribeButtonState extends State { child: Row( children: [ if (subscribed && notificationIcon != null) - NamidaLoadingSwitcher( - size: iconSize, - builder: (startLoading, stopLoading, isLoading) => NamidaIconButton( - horizontalPadding: 4.0, - onPressed: () { - final info = widget.mainChannelInfo.value; - if (info == null) return; - _onNotificationsTap(); - }, - icon: null, - child: notificationIcon, - ), + NamidaIconButton( + horizontalPadding: 4.0, + onPressed: () { + final info = widget.mainChannelInfo.value; + if (info == null) return; + _onNotificationsTap(); + }, + icon: null, + child: notificationIcon, ), NamidaLoadingSwitcher( size: 24.0, - builder: (startLoading, stopLoading, isLoading) => TextButton( + builder: (loadingController) => TextButton( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8.0), tapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -475,8 +474,8 @@ class _YTSubscribeButtonState extends State { } _onChangeSubscribeStatus( subscribed, - startLoading, - stopLoading, + loadingController.startLoading, + loadingController.stopLoading, ); }, ), diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index f87e94aa..df30d867 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -547,7 +547,7 @@ class YoutubeMiniPlayerState extends State { smallIconWidget: FittedBox( child: NamidaLoadingSwitcher( size: 24.0, - builder: (startLoading, stopLoading, isLoading) => NamidaRawLikeButton( + builder: (loadingController) => NamidaRawLikeButton( isLiked: isUserLiked, likedIcon: Broken.like_filled, normalIcon: Broken.like_1, @@ -559,8 +559,8 @@ class YoutubeMiniPlayerState extends State { page: page, isActive: isLiked, action: isLiked ? LikeAction.removeLike : LikeAction.addLike, - onStart: startLoading, - onEnd: stopLoading, + onStart: loadingController.startLoading, + onEnd: loadingController.stopLoading, ), ); }, @@ -585,7 +585,7 @@ class YoutubeMiniPlayerState extends State { smallIconWidget: FittedBox( child: NamidaLoadingSwitcher( size: 24.0, - builder: (startLoading, stopLoading, isLoading) => NamidaRawLikeButton( + builder: (loadingController) => NamidaRawLikeButton( isLiked: isUserDisLiked, likedIcon: Broken.dislike_filled, normalIcon: Broken.dislike, @@ -597,8 +597,8 @@ class YoutubeMiniPlayerState extends State { page: page, isActive: isDisLiked, action: isDisLiked ? LikeAction.removeDislike : LikeAction.addDislike, - onStart: startLoading, - onEnd: stopLoading, + onStart: loadingController.startLoading, + onEnd: loadingController.stopLoading, ), ); }, diff --git a/pubspec.yaml b/pubspec.yaml index b5c4bec9..9a0fb8b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 3.8.36-beta+240805147 +version: 3.8.4-beta+240805147 environment: sdk: ">=3.4.0 <4.0.0"