From e82f32abfba2208c3de804c91b39cef39491d6f0 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Tue, 25 Jun 2024 21:38:18 +0300 Subject: [PATCH] feat(yt): comments sort (top, newest) this required redesign for comments header, which required Widget implementation for PullToRefresh also smol fixes heh --- lib/base/audio_handler.dart | 3 +- lib/base/pull_to_refresh.dart | 48 ++- lib/core/namida_converter_ext.dart | 9 + lib/core/translations/static_strings.dart | 2 + .../controller/youtube_info_controller.dart | 2 +- lib/youtube/pages/youtube_page.dart | 1 + lib/youtube/pages/yt_channel_subpage.dart | 1 + lib/youtube/pages/yt_channels_page.dart | 1 + lib/youtube/widgets/yt_thumbnail.dart | 7 - lib/youtube/youtube_miniplayer.dart | 62 +-- .../yt_miniplayer_comments_subpage.dart | 369 ++++++++++-------- pubspec.yaml | 2 +- 12 files changed, 286 insertions(+), 221 deletions(-) diff --git a/lib/base/audio_handler.dart b/lib/base/audio_handler.dart index e2284ff0..eae0ae6b 100644 --- a/lib/base/audio_handler.dart +++ b/lib/base/audio_handler.dart @@ -1615,6 +1615,7 @@ extension TrackToAudioSourceMediaItem on Selectable { MediaItem toMediaItem(int currentIndex, int queueLength) { final tr = track.toTrackExt(); final artist = tr.originalArtist == '' ? UnknownTags.ARTIST : tr.originalArtist; + final imagePage = tr.pathToImage; return MediaItem( id: tr.path, title: tr.title, @@ -1625,7 +1626,7 @@ extension TrackToAudioSourceMediaItem on Selectable { album: tr.hasUnknownAlbum ? '' : tr.album, genre: tr.originalGenre, duration: Duration(seconds: tr.duration), - artUri: Uri.file(File(tr.pathToImage).existsSync() ? tr.pathToImage : AppPaths.NAMIDA_LOGO), + artUri: Uri.file(File(imagePage).existsSync() ? imagePage : AppPaths.NAMIDA_LOGO), ); } } diff --git a/lib/base/pull_to_refresh.dart b/lib/base/pull_to_refresh.dart index ede1841a..c05ab39c 100644 --- a/lib/base/pull_to_refresh.dart +++ b/lib/base/pull_to_refresh.dart @@ -3,6 +3,48 @@ import 'package:flutter/material.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/utils.dart'; +typedef PullToRefreshCallback = Future Function(); +const double _defaultMaxDistance = 128.0; + +class PullToRefresh extends StatefulWidget { + final Widget child; + final ScrollController controller; + final PullToRefreshCallback onRefresh; + final double maxDistance; + + const PullToRefresh({ + super.key, + required this.child, + required this.controller, + required this.onRefresh, + this.maxDistance = _defaultMaxDistance, + }); + + @override + State createState() => _PullToRefreshState(); +} + +class _PullToRefreshState extends State with TickerProviderStateMixin, PullToRefreshMixin { + @override + double get maxDistance => widget.maxDistance; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) => onPointerMove(widget.controller, event), + onPointerUp: (_) => onRefresh(widget.onRefresh), + onPointerCancel: (_) => onVerticalDragFinish(), + child: Stack( + alignment: Alignment.topCenter, + children: [ + widget.child, + pullToRefreshWidget, + ], + ), + ); + } +} + mixin PullToRefreshMixin on State implements TickerProvider { bool enablePullToRefresh = true; @@ -16,6 +58,8 @@ mixin PullToRefreshMixin on State implements Ticker final _minTrigger = 20; num get pullNormalizer => 100; + final double maxDistance = _defaultMaxDistance; + bool? _isDraggingVertically; double _distanceDragged = 0; bool _onVerticalDragUpdate(double dy) { @@ -48,7 +92,7 @@ mixin PullToRefreshMixin on State implements Ticker } bool _isRefreshing = false; - Future onRefresh(Future Function() execute) async { + Future onRefresh(PullToRefreshCallback execute) async { if (!enablePullToRefresh) return; onVerticalDragFinish(); if (animation.value != 1 || _isRefreshing) return; @@ -78,7 +122,7 @@ mixin PullToRefreshMixin on State implements Ticker const multiplier = 4.5; const minus = multiplier / 3; return Padding( - padding: EdgeInsets.only(top: 12.0 + p * 128.0), + padding: EdgeInsets.only(top: 12.0 + p * maxDistance), child: Transform.rotate( angle: (p * multiplier) - minus, child: AnimatedBuilder( diff --git a/lib/core/namida_converter_ext.dart b/lib/core/namida_converter_ext.dart index 7dd393a2..774f0451 100644 --- a/lib/core/namida_converter_ext.dart +++ b/lib/core/namida_converter_ext.dart @@ -7,6 +7,7 @@ import 'package:playlist_manager/module/playlist_id.dart'; import 'package:youtipie/class/result_wrapper/playlist_result.dart'; import 'package:youtipie/class/streams/audio_stream.dart'; import 'package:youtipie/class/streams/video_stream.dart'; +import 'package:youtipie/core/enum.dart'; import 'package:youtipie/youtipie.dart'; import 'package:namida/class/faudiomodel.dart'; @@ -685,6 +686,10 @@ extension YTSeekActionModeUtils on YTSeekActionMode { String toText() => _NamidaConverters.inst.getTitle(this); } +extension CommentsSortTypeUtils on CommentsSortType { + String toText() => _NamidaConverters.inst.getTitle(this); +} + extension RouteUtils on NamidaRoute { List tracksListInside() { final iter = tracksInside(); @@ -1299,6 +1304,10 @@ class _NamidaConverters { YTSeekActionMode.expandedMiniplayer: lang.EXPANDED_MINIPLAYER, YTSeekActionMode.all: lang.ALL, }, + CommentsSortType: { + CommentsSortType.top: lang.TOP, + CommentsSortType.newest: lang.NEWEST, + }, }; // ==================================================== diff --git a/lib/core/translations/static_strings.dart b/lib/core/translations/static_strings.dart index 18696951..0409cd36 100644 --- a/lib/core/translations/static_strings.dart +++ b/lib/core/translations/static_strings.dart @@ -5,4 +5,6 @@ part of 'language.dart'; mixin _StaticStrings { final NO_NETWORK_AVAILABLE_TO_FETCH_VIDEO_PAGE = 'No Network Available to fetch video page'; final DID_YOU_MEAN = 'Did you mean'; + final TOP = 'Top'; + final NEWEST = 'Newest'; } diff --git a/lib/youtube/controller/youtube_info_controller.dart b/lib/youtube/controller/youtube_info_controller.dart index 54fa7528..7913eee6 100644 --- a/lib/youtube/controller/youtube_info_controller.dart +++ b/lib/youtube/controller/youtube_info_controller.dart @@ -38,7 +38,7 @@ part 'info_controllers/yt_video_info_controller.dart'; part 'youtube_current_info.dart'; class YoutubeInfoController { - YoutubeInfoController._(); + const YoutubeInfoController._(); static const video = _VideoInfoController(); static const playlist = YoutiPie.playlist; diff --git a/lib/youtube/pages/youtube_page.dart b/lib/youtube/pages/youtube_page.dart index c5374416..df5bf0c4 100644 --- a/lib/youtube/pages/youtube_page.dart +++ b/lib/youtube/pages/youtube_page.dart @@ -48,6 +48,7 @@ class _YoutubePageState extends State with AutomaticKeepAliveClient transparent: false, shimmerEnabled: true, child: ListView.builder( + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: feed.length, shrinkWrap: true, diff --git a/lib/youtube/pages/yt_channel_subpage.dart b/lib/youtube/pages/yt_channel_subpage.dart index d1908f14..2135cca4 100644 --- a/lib/youtube/pages/yt_channel_subpage.dart +++ b/lib/youtube/pages/yt_channel_subpage.dart @@ -321,6 +321,7 @@ class _YTChannelSubpageState extends YoutubeChannelController ? ShimmerWrapper( shimmerEnabled: true, child: ListView.builder( + padding: EdgeInsets.zero, itemCount: 15, itemBuilder: (context, index) { return const YoutubeVideoCardDummy( diff --git a/lib/youtube/pages/yt_channels_page.dart b/lib/youtube/pages/yt_channels_page.dart index 2b12d98e..91a1d7ac 100644 --- a/lib/youtube/pages/yt_channels_page.dart +++ b/lib/youtube/pages/yt_channels_page.dart @@ -362,6 +362,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController with LoadingItemsDe Color? smallBoxDynamicColor; final _thumbnailNotFound = false.obs; - bool get canFetchYTImage => widget.videoId != null || widget.customUrl != null; - bool get canFetchImage => widget.localImagePath != null || canFetchYTImage; - Timer? _dontTouchMeImFetchingThumbnail; @override @@ -106,8 +101,6 @@ class _YoutubeThumbnailState extends State with LoadingItemsDe _dontTouchMeImFetchingThumbnail = null; _dontTouchMeImFetchingThumbnail = Timer(const Duration(seconds: 8), () {}); - imagePath = widget.localImagePath; - void onThumbnailNotFound() => _thumbnailNotFound.value = true; if (imagePath == null) { diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 0a8bb436..706d8d48 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -908,59 +908,14 @@ class YoutubeMiniPlayerState extends State { rx: settings.ytTopComments, builder: (ytTopComments) { if (ytTopComments) return const SliverToBoxAdapter(); - return ObxO( - rx: YoutubeInfoController.current.currentComments, - builder: (comments) { - final count = comments?.commentsCount; - return SliverToBoxAdapter( - child: Padding( - key: Key("${currentId}_comments_header"), - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon(Broken.document), - const SizedBox(width: 8.0), - Text( - [ - lang.COMMENTS, - if (count != null) count.formatDecimalShort(), - ].join(' • '), - style: mainTextTheme.displayLarge, - textAlign: TextAlign.start, - ), - const Spacer(), - ObxO( - rx: YoutubeInfoController.current.isCurrentCommentsFromCache, - builder: (commFromCache) { - commFromCache ??= false; - return NamidaIconButton( - // key: Key(currentId), - tooltip: commFromCache ? () => lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async => await YoutubeInfoController.current.updateCurrentComments( - currentId, - newSortType: YoutubeMiniplayerUiController.inst.currentCommentSort.value, - initial: true, - ), - child: commFromCache - ? const StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - ) - : Icon( - Broken.refresh, - color: defaultIconColor, - ), - ); - }, - ), - ], - ), - ), - ); - }, + return SliverToBoxAdapter( + key: Key("${currentId}_comments_header"), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: YoutubeCommentsHeader( + displayBackButton: false, + ), + ), ); }, ), @@ -977,6 +932,7 @@ class YoutubeMiniPlayerState extends State { transparent: false, shimmerEnabled: true, child: ListView.builder( + padding: EdgeInsets.zero, // key: Key(currentId), physics: const NeverScrollableScrollPhysics(), itemCount: 10, diff --git a/lib/youtube/yt_miniplayer_comments_subpage.dart b/lib/youtube/yt_miniplayer_comments_subpage.dart index 9a22a100..775239d9 100644 --- a/lib/youtube/yt_miniplayer_comments_subpage.dart +++ b/lib/youtube/yt_miniplayer_comments_subpage.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:youtipie/core/enum.dart'; +import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/controller/connectivity.dart'; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; @@ -12,6 +14,7 @@ import 'package:namida/core/utils.dart'; import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/controller/yt_miniplayer_ui_controller.dart'; import 'package:namida/youtube/widgets/yt_comment_card.dart'; @@ -39,171 +42,225 @@ class _YTMiniplayerCommentsSubpageState extends State lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async { - if (!ConnectivityController.inst.hasConnection) return; - try { - sc.jumpTo(0); - } catch (_) {} - if (currentId != null) { - await YoutubeInfoController.current.updateCurrentComments( - currentId, - newSortType: YoutubeMiniplayerUiController.inst.currentCommentSort.value, - initial: true, - ); - } - }, - child: isCurrentCommentsFromCache - ? const StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - ) - : Icon( - Broken.refresh, - color: context.defaultIconColor(), - ), + child: ObxO( + rx: Player.inst.currentItem, + builder: (currentItem) { + if (currentItem is! YoutubeID) return const SizedBox(); + final currentId = currentItem.id; + return Column( + children: [ + DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 12.0, + color: context.theme.secondaryHeaderColor.withOpacity(0.5), + ) + ], + ), + child: const Column( + children: [ + SizedBox(height: 12.0), + YoutubeCommentsHeader( + displayBackButton: true, + ), + SizedBox(height: 12.0), + ], + ), + ), + Expanded( + child: NamidaScrollbar( + controller: sc, + child: PullToRefresh( + maxDistance: 64.0, + controller: sc, + onRefresh: () async { + if (!ConnectivityController.inst.hasConnection) return; + try { + sc.jumpTo(0); + } catch (_) {} + await YoutubeInfoController.current.updateCurrentComments( + currentId, + newSortType: YoutubeMiniplayerUiController.inst.currentCommentSort.value, + initial: true, ); }, - ), - const SizedBox(width: 8.0), - ], - ), - ), - ), - Expanded( - child: NamidaScrollbar( - controller: sc, - child: LazyLoadListView( - onReachingEnd: () async => currentId == null ? null : await YoutubeInfoController.current.updateCurrentComments(currentId), - extend: 400, - scrollController: sc, - listview: (controller) => CustomScrollView( - restorationId: currentId, - physics: const ClampingScrollPhysicsModified(), - controller: controller, - slivers: [ - ObxO( - rx: YoutubeInfoController.current.isLoadingInitialComments, - builder: (loadingInitial) { - if (loadingInitial) { - return SliverToBoxAdapter( - key: Key("${currentId}_comments_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - itemCount: 10, - shrinkWrap: true, - itemBuilder: (context, index) { - const comment = null; - return const YTCommentCard( - key: Key("${comment == null}"), - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, - ); - }, - ), - ), - ); - } - return ObxO( - rx: YoutubeInfoController.current.currentComments, - builder: (comments) { - if (comments == null) return const SliverToBoxAdapter(); - return SliverList.builder( - key: Key("${currentId}_comments"), - itemCount: comments.length, - itemBuilder: (context, i) { - final comment = comments[i]; - return ShimmerWrapper( - transparent: false, - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: comment == null, - child: YTCommentCard( - key: Key("${comment == null}_${comment?.commentId}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, + child: LazyLoadListView( + onReachingEnd: () => YoutubeInfoController.current.updateCurrentComments(currentId), + extend: 400, + scrollController: sc, + listview: (controller) => CustomScrollView( + physics: const ClampingScrollPhysicsModified(), + controller: controller, + slivers: [ + ObxO( + rx: YoutubeInfoController.current.isLoadingInitialComments, + builder: (loadingInitial) { + if (loadingInitial) { + return SliverToBoxAdapter( + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + shrinkWrap: true, + itemBuilder: (context, index) { + const comment = null; + return const YTCommentCard( + key: Key("${comment == null}"), + margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ); + }, + ), ), ); - }, - ); - }, - ); - }, - ), - ObxO( - rx: YoutubeInfoController.current.isLoadingMoreComments, - builder: (isLoadingMoreComments) => isLoadingMoreComments - ? const SliverPadding( - padding: EdgeInsets.all(12.0), - sliver: SliverToBoxAdapter( - child: Center( - child: LoadingIndicator(), - ), - ), - ) - : const SliverToBoxAdapter(), + } + return ObxO( + rx: YoutubeInfoController.current.currentComments, + builder: (comments) { + if (comments == null) return const SliverToBoxAdapter(); + return SliverList.builder( + itemCount: comments.length, + itemBuilder: (context, i) { + final comment = comments[i]; + return ShimmerWrapper( + transparent: false, + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: comment == null, + child: YTCommentCard( + key: Key("${comment == null}_${comment?.commentId}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ), + ); + }, + ); + }, + ); + }, + ), + ObxO( + rx: YoutubeInfoController.current.isLoadingMoreComments, + builder: (isLoadingMoreComments) => isLoadingMoreComments + ? const SliverPadding( + padding: EdgeInsets.all(12.0), + sliver: SliverToBoxAdapter( + child: Center( + child: LoadingIndicator(), + ), + ), + ) + : const SliverToBoxAdapter(), + ), + const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight + 12.0)) + ], + ), ), - const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight + 12.0)) - ], + ), ), ), - ), - ), - ], + ], + ); + }, ), ); } } + +class YoutubeCommentsHeader extends StatelessWidget { + final bool displayBackButton; + const YoutubeCommentsHeader({super.key, required this.displayBackButton}); + + @override + Widget build(BuildContext context) { + final commentsIconColor = context.theme.iconTheme.color; + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (displayBackButton) const SizedBox(width: 12.0), + if (displayBackButton) + NamidaIconButton( + horizontalPadding: 0, + icon: Broken.arrow_left_2, + onPressed: NamidaNavigator.inst.popPage, + ), + const SizedBox(width: 12.0), + ObxO( + rx: YoutubeInfoController.current.isCurrentCommentsFromCache, + builder: (isCurrentCommentsFromCache) => (isCurrentCommentsFromCache ?? false) + ? StackedIcon( + baseIcon: Broken.document, + secondaryIcon: Broken.global, + iconSize: 22.0, + secondaryIconSize: 12.0, + baseIconColor: commentsIconColor, + secondaryIconColor: commentsIconColor, + ) + : const Icon( + Broken.document, + size: 22.0, + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: ObxO( + rx: YoutubeInfoController.current.currentComments, + builder: (comments) { + final count = comments?.commentsCount; + return Text( + [ + lang.COMMENTS, + if (count != null) count.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displayMedium, + textAlign: TextAlign.start, + ); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const SizedBox(width: 8.0), + ...CommentsSortType.values.map( + (s) => ObxO( + rx: YoutubeMiniplayerUiController.inst.currentCommentSort, + builder: (currentCommentSort) => NamidaInkWell( + borderRadius: 8.0, + margin: const EdgeInsets.symmetric(horizontal: 4.0), + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + bgColor: currentCommentSort == s ? context.theme.colorScheme.secondaryContainer : context.theme.cardColor, + onTap: () async { + if (YoutubeMiniplayerUiController.inst.currentCommentSort.value == s) return; + + final currentItem = Player.inst.currentItem.value; + if (currentItem is! YoutubeID) return; + final currentId = currentItem.id; + + YoutubeMiniplayerUiController.inst.currentCommentSort.value = s; + await YoutubeInfoController.current.updateCurrentComments( + currentId, + newSortType: s, + initial: true, + ); + }, + child: Text( + s.toText(), + style: context.textTheme.displayMedium, + ), + ), + ), + ), + const SizedBox(width: 8.0), + ], + ), + const SizedBox(width: 8.0), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 40fcb511..cf5ae21f 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: 2.8.1-beta+240625124 +version: 2.8.2-beta+240625185 environment: sdk: ">=3.4.0 <4.0.0"