diff --git a/lib/youtube/pages/youtube_feed_page.dart b/lib/youtube/pages/youtube_feed_page.dart index 1b012056..a5d1c16d 100644 --- a/lib/youtube/pages/youtube_feed_page.dart +++ b/lib/youtube/pages/youtube_feed_page.dart @@ -32,6 +32,47 @@ class YoutubeHomeFeedPage extends StatelessWidget { thumbnailWidth: thumbnailWidth, thumbnailHeight: thumbnailHeight, ), + sliverListBuilder: (feed, itemBuilder, dummyCard) { + return SliverVariedExtentList.builder( + itemExtentBuilder: (index, dimensions) { + if (feed.shortsSection.relatedItemsShortsData[index] != null) return 64.0 * 3 + 24.0 * 2; + final item = feed.items[index]; + if (item is StreamInfoItemShort) return 0; + return thumbnailItemExtent; + }, + itemCount: feed.items.length, + itemBuilder: (context, index) { + final shortSection = feed.shortsSection.relatedItemsShortsData[index]; + if (shortSection != null) { + const height = 64.0 * 3; + const width = height * (9 / 16 * 1.2); + const hPadding = 4.0; + return SizedBox( + height: height, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 24.0 / 6, horizontal: 4.0), + scrollDirection: Axis.horizontal, + itemExtent: width + hPadding * 2, + itemCount: shortSection.length, + itemBuilder: (context, index) { + final shortIndex = shortSection[index]; + final short = feed.items[shortIndex] as StreamInfoItemShort; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: hPadding), + child: YoutubeShortVideoTallCard( + short: short, + thumbnailWidth: width, + thumbnailHeight: height, + ), + ); + }, + ), + ); + } + return itemBuilder(feed.items[index], index, feed); + }, + ); + }, itemBuilder: (item, i, _) { return switch (item.runtimeType) { const (StreamInfoItem) => YoutubeVideoCard( 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 c60f84a7..c3de1fdd 100644 --- a/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart +++ b/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart @@ -17,6 +17,8 @@ import 'package:namida/ui/widgets/settings/extra_settings.dart'; import 'package:namida/youtube/controller/youtube_account_controller.dart'; import 'package:namida/youtube/pages/user/youtube_account_manage_page.dart'; +typedef YoutubeMainPageFetcherItemBuilder = Widget? Function(T item, int index, W list); + class YoutubeMainPageFetcherAccBase, T extends MapSerializable> extends StatefulWidget { final bool transparentShimmer; final String title; @@ -24,7 +26,8 @@ class YoutubeMainPageFetcherAccBase, T extends final Future Function(ExecuteDetails details) networkFetcher; final Widget dummyCard; final double itemExtent; - final Widget? Function(T item, int index, W list) itemBuilder; + final YoutubeMainPageFetcherItemBuilder itemBuilder; + final SliverMultiBoxAdaptorWidget? Function(W list, YoutubeMainPageFetcherItemBuilder itemBuilder, Widget dummyCard)? sliverListBuilder; const YoutubeMainPageFetcherAccBase({ super.key, @@ -35,6 +38,7 @@ class YoutubeMainPageFetcherAccBase, T extends required this.dummyCard, required this.itemExtent, required this.itemBuilder, + this.sliverListBuilder, }); @override @@ -195,14 +199,15 @@ class _YoutubePageState, T extends MapSerializa ) : listItems == null ? const SliverToBoxAdapter() - : SliverFixedExtentList.builder( - itemCount: listItems.items.length, - itemExtent: widget.itemExtent, - itemBuilder: (context, i) { - final item = listItems.items[i]; - return widget.itemBuilder(item, i, listItems); - }, - ), + : widget.sliverListBuilder?.call(listItems, widget.itemBuilder, widget.dummyCard) ?? + SliverFixedExtentList.builder( + itemCount: listItems.items.length, + itemExtent: widget.itemExtent, + itemBuilder: (context, i) { + final item = listItems.items[i]; + return widget.itemBuilder(item, i, listItems); + }, + ), SliverToBoxAdapter( child: ObxO( rx: _isLoadingNext, diff --git a/lib/youtube/widgets/yt_video_card.dart b/lib/youtube/widgets/yt_video_card.dart index 0b40da90..a5d22ea0 100644 --- a/lib/youtube/widgets/yt_video_card.dart +++ b/lib/youtube/widgets/yt_video_card.dart @@ -11,6 +11,7 @@ import 'package:namida/controller/player_controller.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.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/class/youtube_id.dart'; import 'package:namida/youtube/functions/yt_playlist_utils.dart'; @@ -209,6 +210,90 @@ class YoutubeShortVideoCard extends StatelessWidget { } } +class YoutubeShortVideoTallCard extends StatelessWidget { + final StreamInfoItemShort short; + final double thumbnailWidth; + final double? thumbnailHeight; + + const YoutubeShortVideoTallCard({ + super.key, + required this.short, + required this.thumbnailWidth, + required this.thumbnailHeight, + }); + + List getMenuItems() { + final videoId = short.id; + return YTUtils.getVideoCardMenuItems( + videoId: videoId, + url: short.buildUrl(), + channelID: null, + playlistID: null, + idsNamesLookup: {videoId: short.title}, + ); + } + + Future _onShortTap() => _VideoCardUtils.onVideoTap(videoId: short.id); + + @override + Widget build(BuildContext context) { + final videoId = short.id; + final title = short.title; + final viewsCountText = short.viewsText; + final thumbnail = short.liveThumbs.pick()?.url; + + return NamidaPopupWrapper( + openOnTap: false, + childrenDefault: getMenuItems, + child: NamidaInkWell( + bgColor: context.theme.cardColor, + borderRadius: 8.0, + onTap: _onShortTap, + child: YoutubeThumbnail( + key: Key(videoId), + borderRadius: 8.0, + videoId: videoId, + customUrl: thumbnail, + width: thumbnailWidth, + height: thumbnailHeight, + isImportantInCache: false, + type: ThumbnailType.video, + onTopWidgets: (color) { + return [ + Positioned( + bottom: 0, + left: 0, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: context.textTheme.displayMedium?.copyWith(fontSize: 12.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + viewsCountText, + style: context.textTheme.displaySmall?.copyWith(fontSize: 11.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ]; + }, + ), + ), + ); + } +} + class YoutubeVideoCardDummy extends StatelessWidget { final double? thumbnailWidth; final double? thumbnailHeight; diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index c71cbb76..efcc5abc 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -841,11 +841,44 @@ class YoutubeMiniPlayerState extends State { ), ), ) - : SliverFixedExtentList.builder( + : SliverVariedExtentList.builder( key: Key("${currentId}_feedlist"), - itemExtent: relatedThumbnailItemExtent, + itemExtentBuilder: (index, dimensions) { + if (page.relatedVideosResult.shortsSection.relatedItemsShortsData[index] != null) return 64.0 * 3 + 24.0 * 2; + final item = page.relatedVideosResult.items[index]; + if (item is StreamInfoItemShort) return 0; + return relatedThumbnailItemExtent; + }, itemCount: page.relatedVideosResult.items.length, itemBuilder: (context, index) { + final shortSection = page.relatedVideosResult.shortsSection.relatedItemsShortsData[index]; + if (shortSection != null) { + const height = 64.0 * 3; + const width = height * (9 / 16 * 1.2); + const hPadding = 4.0; + return SizedBox( + height: height, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 24.0 / 6, horizontal: 4.0), + scrollDirection: Axis.horizontal, + itemExtent: width + hPadding * 2, + itemCount: shortSection.length, + itemBuilder: (context, index) { + final shortIndex = shortSection[index]; + final short = page.relatedVideosResult.items[shortIndex] as StreamInfoItemShort; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: hPadding), + child: YoutubeShortVideoTallCard( + short: short, + thumbnailWidth: width, + thumbnailHeight: height, + ), + ); + }, + ), + ); + } + final item = page.relatedVideosResult.items[index]; return switch (item.runtimeType) { const (StreamInfoItem) => YoutubeVideoCard( diff --git a/pubspec.yaml b/pubspec.yaml index cbeeb66b..923b581f 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.1.6-beta+240710229 +version: 3.1.7-beta+240711002 environment: sdk: ">=3.4.0 <4.0.0"