diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index bce3c652..3de60d2b 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -15,8 +15,6 @@ on: jobs: build_android: runs-on: ubuntu-latest - env: - ACCESS_TOKEN: ${{ secrets.PAT }} steps: - uses: actions/checkout@v3 @@ -90,11 +88,11 @@ jobs: repository: namidaco/namida-snapshots tag_name: ${{ steps.extract_version.outputs.version}} files: | - build_final_signed/* + build_final/* token: ${{ secrets.SNAPSHOTS_REPO_SECRET }} - name: Upload all APKs uses: actions/upload-artifact@v3 with: name: all-apks - path: build_final_signed/** + path: build_final/** diff --git a/lib/base/audio_handler.dart b/lib/base/audio_handler.dart index eae0ae6b..7c99e154 100644 --- a/lib/base/audio_handler.dart +++ b/lib/base/audio_handler.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:basic_audio_handler/basic_audio_handler.dart'; -import 'package:flutter/scheduler.dart'; import 'package:just_audio/just_audio.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:playlist_manager/module/playlist_id.dart'; @@ -153,13 +152,21 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ================================================================================= void refreshNotification([Q? item, YoutubeIDToMediaItemCallback? youtubeIdMediaItem]) { - final exectuteOn = item ?? currentItem.value; + Q? exectuteOn = item ?? currentItem.value; + Duration? knownDur; + if (item != null) { + exectuteOn = item; + } else { + exectuteOn = currentItem.value; + knownDur = currentItemDuration.value; + } exectuteOn?._execute( selectable: (finalItem) { _notificationUpdateItemSelectable( item: finalItem, isItemFavourite: finalItem.track.isFavourite, itemIndex: currentIndex.value, + duration: knownDur, ); }, youtubeID: (finalItem) { @@ -171,7 +178,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { (int index, int queueLength) { final streamInfo = YoutubeInfoController.current.currentYTStreams.value?.info; final thumbnail = finalItem.getThumbnailSync(); - return finalItem.toMediaItem(streamInfo, thumbnail, index, queueLength); + return finalItem.toMediaItem(streamInfo, thumbnail, index, queueLength, knownDur); }, ); }, @@ -182,8 +189,9 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { required Selectable item, required bool isItemFavourite, required int itemIndex, + required Duration? duration, }) { - mediaItem.add(item.toMediaItem(currentIndex.value, currentQueue.value.length)); + mediaItem.add(item.toMediaItem(currentIndex.value, currentQueue.value.length, duration)); playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex.value), isItemFavourite, itemIndex)); } @@ -523,10 +531,12 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { } }); + Duration? duration; + Future setPls() async { if (!File(tr.path).existsSync()) throw PathNotFoundException(tr.path, const OSError(), 'Track file not found or couldn\'t be accessed.'); final dur = await setSource( - tr.toAudioSource(currentIndex.value, currentQueue.value.length), + tr.toAudioSource(currentIndex.value, currentQueue.value.length, duration), item: pi, startPlaying: startPlaying, videoOptions: initialVideo == null @@ -546,8 +556,6 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { return dur; } - Duration? duration; - bool checkInterrupted() { if (item.track != currentItem.value) { return true; @@ -870,12 +878,21 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { final hadCachedComments = YoutubeInfoController.current.updateCurrentCommentsSync(item.id); Duration? duration; + Duration? notificationDuration; + VideoStreamInfo? notificationVideoInfo; + File? notificationVideoThumbnail; bool checkInterrupted() { if (item != currentItem.value) { return true; } else { - if (duration != null) _currentItemDuration.value = duration; + if (duration != null) { + _currentItemDuration.value = duration; + if (notificationDuration == null) { + notificationDuration = duration; + refreshNotification(pi, (index, ql) => item.toMediaItem(notificationVideoInfo, notificationVideoThumbnail, index, ql, duration)); + } + } return false; } } @@ -883,29 +900,24 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { Future fetchFullVideoPage() async { await YoutubeInfoController.current.updateVideoPage( item.id, - forceRequestPage: !hadCachedVideoPage, - forceRequestComments: !hadCachedComments, + requestPage: !hadCachedVideoPage, + requestComments: !hadCachedComments, ); } - VideoStreamInfo? info; - File? videoThumbnail; - bool notificationDidRefreshInfo = false; - bool notificationDidRefreshThumbnail = false; - void onInfoOrThumbObtained({VideoStreamInfo? info, File? thumbnail, bool forceRefreshNoti = false}) { - if (forceRefreshNoti == false && notificationDidRefreshInfo && notificationDidRefreshThumbnail) return; + void onInfoOrThumbObtained({VideoStreamInfo? info, File? thumbnail}) { if (checkInterrupted()) return; - if (info != null) notificationDidRefreshInfo = true; - if (thumbnail != null) notificationDidRefreshThumbnail = true; - refreshNotification(pi, (index, ql) => item.toMediaItem(info, thumbnail, index, ql)); + notificationVideoInfo ??= info; // we assign cuz later some functions can depend on this + notificationVideoThumbnail ??= thumbnail; + refreshNotification(pi, (index, ql) => item.toMediaItem(notificationVideoInfo, notificationVideoThumbnail, index, ql, duration)); } - info = streamsResult?.info; - videoThumbnail = item.getThumbnailSync(); - if (info != null || videoThumbnail != null) { - onInfoOrThumbObtained(info: info, thumbnail: videoThumbnail); + notificationVideoInfo = streamsResult?.info; + notificationVideoThumbnail = item.getThumbnailSync(); + if (notificationVideoInfo != null || notificationVideoThumbnail != null) { + onInfoOrThumbObtained(info: notificationVideoInfo, thumbnail: notificationVideoThumbnail); } - if (videoThumbnail == null) { + if (notificationVideoThumbnail == null) { ThumbnailManager.inst.getYoutubeThumbnailAndCache(id: item.id).then((thumbFile) => onInfoOrThumbObtained(thumbnail: thumbFile)); } @@ -964,7 +976,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { /// different then it will be set later after fetching. playedFromCacheDetails = await _trySetYTVideoWithoutConnection( item: item, - mediaItemFn: () => item.toMediaItem(info, videoThumbnail, index, currentQueue.value.length), + mediaItemFn: () => item.toMediaItem(notificationVideoInfo, notificationVideoThumbnail, index, currentQueue.value.length, duration), checkInterrupted: () => item != currentItem.value, index: index, canPlayAudioOnly: canPlayAudioOnlyFromCache, @@ -1017,7 +1029,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { snackyy(message: 'Error getting streams', top: false, isError: true); return null; }); - onInfoOrThumbObtained(info: streamsResult?.info, forceRefreshNoti: false /* we may need to force refresh if info could have changed */); + onInfoOrThumbObtained(info: streamsResult?.info); if (checkInterrupted()) return; YoutubeInfoController.current.currentYTStreams.value = streamsResult; } else { @@ -1029,11 +1041,16 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { fetchFullVideoPage(); + notificationVideoInfo = streamsResult?.info; + final audiostreams = streamsResult?.audioStreams ?? []; final videoStreams = streamsResult?.videoStreams ?? []; - info = streamsResult?.info; - if (info == null && audiostreams.isEmpty && videoStreams.isEmpty) return; + if (audiostreams.isEmpty) { + snackyy(message: 'Error playing video, empty audio streams', top: false, isError: true); + return; + } + if (checkInterrupted()) return; final prefferedVideoStream = _isAudioOnlyPlayback || videoStreams.isEmpty ? null : YoutubeController.inst.getPreferredStreamQuality(videoStreams, preferIncludeWebm: false); final prefferedAudioStream = @@ -1108,11 +1125,9 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { } catch (e) { if (checkInterrupted()) return; void showSnackError(String nextAction) { - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - if (item == currentItem.value) { - snackyy(message: 'Error playing video, $nextAction: $e', top: false, isError: true); - } - }); + if (item == currentItem.value) { + snackyy(message: 'Error playing video, $nextAction: $e', top: false, isError: true); + } } showSnackError('trying again'); @@ -1120,7 +1135,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { printy(e, isError: true); playedFromCacheDetails = await _trySetYTVideoWithoutConnection( item: item, - mediaItemFn: () => item.toMediaItem(info, videoThumbnail, index, currentQueue.value.length), + mediaItemFn: () => item.toMediaItem(notificationVideoInfo, notificationVideoThumbnail, index, currentQueue.value.length, duration), checkInterrupted: checkInterrupted, index: index, canPlayAudioOnly: canPlayAudioOnlyFromCache, @@ -1315,6 +1330,7 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { item: finalItem, itemIndex: currentIndex.value, isItemFavourite: newStat, + duration: currentItemDuration.value, ); }, youtubeID: (finalItem) {}, @@ -1605,14 +1621,14 @@ class NamidaAudioVideoHandler extends BasicAudioHandler { // ----------------------- Extensions -------------------------- extension TrackToAudioSourceMediaItem on Selectable { - UriAudioSource toAudioSource(int currentIndex, int queueLength) { + UriAudioSource toAudioSource(int currentIndex, int queueLength, Duration? duration) { return AudioSource.uri( Uri.file(track.path), - tag: toMediaItem(currentIndex, queueLength), + tag: toMediaItem(currentIndex, queueLength, duration), ); } - MediaItem toMediaItem(int currentIndex, int queueLength) { + MediaItem toMediaItem(int currentIndex, int queueLength, Duration? duration) { final tr = track.toTrackExt(); final artist = tr.originalArtist == '' ? UnknownTags.ARTIST : tr.originalArtist; final imagePage = tr.pathToImage; @@ -1625,14 +1641,14 @@ extension TrackToAudioSourceMediaItem on Selectable { artist: artist, album: tr.hasUnknownAlbum ? '' : tr.album, genre: tr.originalGenre, - duration: Duration(seconds: tr.duration), + duration: duration ?? Duration(seconds: tr.duration), artUri: Uri.file(File(imagePage).existsSync() ? imagePage : AppPaths.NAMIDA_LOGO), ); } } extension YoutubeIDToMediaItem on YoutubeID { - MediaItem toMediaItem(VideoStreamInfo? videoInfo, File? thumbnail, int currentIndex, int queueLength) { + MediaItem toMediaItem(VideoStreamInfo? videoInfo, File? thumbnail, int currentIndex, int queueLength, Duration? duration) { final vi = videoInfo; final artistAndTitle = vi?.title.splitArtistAndTitle(); final videoName = vi?.title; @@ -1654,7 +1670,7 @@ extension YoutubeIDToMediaItem on YoutubeID { displayTitle: videoName, displaySubtitle: channelName, displayDescription: "${currentIndex + 1}/$queueLength", - duration: vi?.durSeconds?.seconds ?? Duration.zero, + duration: duration ?? vi?.durSeconds?.seconds ?? Duration.zero, artUri: Uri.file((thumbnail != null && thumbnail.existsSync()) ? thumbnail.path : AppPaths.NAMIDA_LOGO), ); } diff --git a/lib/base/pull_to_refresh.dart b/lib/base/pull_to_refresh.dart index c05ab39c..fe7492ef 100644 --- a/lib/base/pull_to_refresh.dart +++ b/lib/base/pull_to_refresh.dart @@ -92,10 +92,15 @@ mixin PullToRefreshMixin on State implements Ticker } bool _isRefreshing = false; - Future onRefresh(PullToRefreshCallback execute) async { + Future onRefresh(PullToRefreshCallback execute, {bool forceShow = false}) async { if (!enablePullToRefresh) return; onVerticalDragFinish(); - if (animation.value != 1 || _isRefreshing) return; + if (_isRefreshing) return; + if (animation.value != 1) { + if (!forceShow) return; + animation.animateTo(1, duration: const Duration(milliseconds: 100)); + } + _isRefreshing = true; _animation2.repeat(); await execute(); diff --git a/lib/base/youtube_channel_controller.dart b/lib/base/youtube_channel_controller.dart index f7189409..5dc4273f 100644 --- a/lib/base/youtube_channel_controller.dart +++ b/lib/base/youtube_channel_controller.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:youtipie/class/channels/channel_page_result.dart'; import 'package:youtipie/class/channels/tabs/channel_tab_videos_result.dart'; +import 'package:youtipie/class/execute_details.dart'; import 'package:youtipie/class/stream_info_item/stream_info_item.dart'; +import 'package:youtipie/youtipie.dart'; import 'package:namida/base/youtube_streams_manager.dart'; import 'package:namida/controller/connectivity.dart'; import 'package:namida/controller/current_color.dart'; +import 'package:namida/core/extensions.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_subscription.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart'; -import 'package:youtipie/youtipie.dart'; abstract class YoutubeChannelController extends State with YoutubeStreamsManager { @override @@ -23,7 +25,7 @@ abstract class YoutubeChannelController extends State< Color? get sortChipBGColor => CurrentColor.inst.color; @override - void onSortChanged(void Function() fn) => setState(fn); + void onSortChanged(void Function() fn) => refreshState(fn); late final ScrollController uploadsScrollController = ScrollController(); YoutubeSubscription? channel; @@ -61,18 +63,19 @@ abstract class YoutubeChannelController extends State< streamsPeakDates = (oldest: DateTime.fromMillisecondsSinceEpoch(oldest), newest: DateTime.fromMillisecondsSinceEpoch(newest)); } - Future fetchChannelStreams(YoutiPieChannelPageResult channelPage) async { + Future fetchChannelStreams(YoutiPieChannelPageResult channelPage, {bool forceRequest = false}) async { final tab = channelPage.tabs.getVideosTab(); if (tab == null) return; final channelID = channelPage.id; - final result = await YoutubeInfoController.channel.fetchChannelTab(channelId: channelID, tab: tab); + final details = forceRequest ? ExecuteDetails.forceRequest() : null; + final result = await YoutubeInfoController.channel.fetchChannelTab(channelId: channelID, tab: tab, details: details); if (result == null) return; - this.channelVideoTab = result; final st = result.items; updatePeakDates(st); YoutubeSubscriptionsController.inst.refreshLastFetchedTime(channelID); - setState(() { + refreshState(() { + this.channelVideoTab = result; isLoadingInitialStreams = false; if (channelID == channel?.channelID) { trySortStreams(); @@ -93,7 +96,7 @@ abstract class YoutubeChannelController extends State< if (didFetch) { if (result.channelId == channel?.channelID) { - setState(trySortStreams); + refreshState(trySortStreams); } } else { if (ConnectivityController.inst.hasConnection) lastLoadingMoreWasEmpty.value = true; diff --git a/lib/controller/backup_controller.dart b/lib/controller/backup_controller.dart index 7db936d5..1714d250 100644 --- a/lib/controller/backup_controller.dart +++ b/lib/controller/backup_controller.dart @@ -55,6 +55,7 @@ class BackupController { AppPaths.SETTINGS_PLAYER, AppPaths.LATEST_QUEUE, AppPaths.YT_LIKES_PLAYLIST, + AppPaths.YT_SUBSCRIPTIONS, AppDirs.PLAYLISTS, AppDirs.HISTORY_PLAYLIST, AppDirs.QUEUES, diff --git a/lib/controller/file_browser.dart b/lib/controller/file_browser.dart index 30c72615..85249933 100644 --- a/lib/controller/file_browser.dart +++ b/lib/controller/file_browser.dart @@ -243,7 +243,7 @@ class _NamidaFileBrowserState extends State<_NamidaF _SortType.size: lang.SIZE, }; - void _sortItems(_SortType? type, bool? reversed, {bool refresh = true}) { + void _sortItems(_SortType? type, bool? reversed, {bool refreshState = true}) { type ??= _sortType.value; reversed ??= _sortReversed.value; @@ -272,7 +272,7 @@ class _NamidaFileBrowserState extends State<_NamidaF _sortType.value = type; _sortReversed.value = reversed; - if (refresh) setState(() {}); + if (refreshState) setState(() {}); } final _showHiddenFiles = false.obs; @@ -344,7 +344,7 @@ class _NamidaFileBrowserState extends State<_NamidaF _isFetching = false; if (isolateRes.$1.isNotEmpty) _currentFiles = isolateRes.$1; if (isolateRes.$2.isNotEmpty) _currentFolders = isolateRes.$2; - _sortItems(null, null, refresh: false); + _sortItems(null, null, refreshState: false); }); if (isolateRes.$3 != null) { snackyy(title: lang.ERROR, message: isolateRes.$3!.toString(), isError: true); @@ -367,6 +367,7 @@ class _NamidaFileBrowserState extends State<_NamidaF setState(() { _currentInfoFiles = res.$1; _currentInfoDirs = res.$2; + if (_sortType.value != _SortType.name) _sortItems(null, null, refreshState: false); }); } _stopInfoIsolates(); diff --git a/lib/controller/settings.player.dart b/lib/controller/settings.player.dart index 74309b1a..79f7e47a 100644 --- a/lib/controller/settings.player.dart +++ b/lib/controller/settings.player.dart @@ -24,7 +24,7 @@ class PlayerSettings with SettingsFileWriter { final seekDurationInSeconds = 5.obs; final seekDurationInPercentage = 2.obs; final isSeekDurationPercentage = false.obs; - final minTrackDurationToRestoreLastPosInMinutes = 5.obs; + final minTrackDurationToRestoreLastPosInMinutes = 20.obs; final interruptionResumeThresholdMin = 2.obs; final volume0ResumeThresholdMin = 5.obs; final enableCrossFade = false.obs; diff --git a/lib/controller/settings_controller.dart b/lib/controller/settings_controller.dart index c81360cd..8c828f43 100644 --- a/lib/controller/settings_controller.dart +++ b/lib/controller/settings_controller.dart @@ -12,12 +12,10 @@ import 'package:namida/core/constants.dart'; import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; -SettingsController get settings => SettingsController.inst; +final settings = _SettingsController._internal(); -class SettingsController with SettingsFileWriter { - static SettingsController get inst => _instance; - static final SettingsController _instance = SettingsController._internal(); - SettingsController._internal(); +class _SettingsController with SettingsFileWriter { + _SettingsController._internal(); EqualizerSettings get equalizer => EqualizerSettings.inst; PlayerSettings get player => PlayerSettings.inst; diff --git a/lib/controller/thumbnail_manager.dart b/lib/controller/thumbnail_manager.dart index a9215866..fe1d3b37 100644 --- a/lib/controller/thumbnail_manager.dart +++ b/lib/controller/thumbnail_manager.dart @@ -164,6 +164,9 @@ class ThumbnailManager { required String? symlinkId, required VoidCallback? onNotFound, }) async { + final activeRequest = _thumbnailDownloader.resultForId(itemId); + if (activeRequest != null) return activeRequest; + final links = []; if (isVideo && (urls == null || urls.isEmpty)) { final yth = YoutiPieVideoThumbnail(itemId); @@ -176,7 +179,7 @@ class ThumbnailManager { if (urls != null) links.addAll(urls); if (links.isEmpty) return null; - final downloaded = await _thumbnailDownloader.download( + return _thumbnailDownloader.download( urls: links, id: itemId, forceRequest: forceRequest, @@ -186,7 +189,6 @@ class ThumbnailManager { isTemp: isTemp, onNotFound: onNotFound, ); - return downloaded; } } @@ -195,6 +197,8 @@ class _YTThumbnailDownloadManager with PortsProvider { final _shouldRetry = {}; // item id final _notFoundThumbnails = {}; // item id + Future? resultForId(String id) => _downloadCompleters[id]?.future; + Future download({ required List urls, required String id, @@ -333,7 +337,7 @@ class _YTThumbnailDownloadManager with PortsProvider { await fileStream.addStream(downloadStream); newFile = destinationFileTemp.renameSync(destinationFile.path); // rename .temp if (symlinkId != null) { - Link.fromUri(Uri.file("${newFile.parent.path}/symlinkId")).create(newFile.path).catchError((_) => Link('')); + Link("${newFile.parent.path}/$symlinkId").create(newFile.path).catchError((_) => Link('')); } if (deleteOldExtracted) { File("${destinationFile.parent}/EXT_${destinationFile.path.getFilename}").delete().catchError((_) => File('')); diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 84cf0f66..d306fd1b 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -156,9 +156,9 @@ class AppPaths { static final SETTINGS_EQUALIZER = '$_USER_DATA/namida_settings_eq.json'; static final SETTINGS_PLAYER = '$_USER_DATA/namida_settings_player.json'; static final TRACKS = '$_USER_DATA/tracks.json'; + static final TRACKS_STATS = '$_USER_DATA/tracks_stats.json'; static final VIDEOS_LOCAL = '$_USER_DATA/local_videos.json'; static final VIDEOS_CACHE = '$_USER_DATA/cache_videos.json'; - static final TRACKS_STATS = '$_USER_DATA/tracks_stats.json'; static final LATEST_QUEUE = '$_USER_DATA/latest_queue.json'; static String get LOGS => _getLogsFile(''); diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index 02432736..837ee215 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -715,6 +715,7 @@ class _YoutubeIDImage extends StatelessWidget { Widget build(BuildContext context) { final width = context.width; return YoutubeThumbnail( + type: ThumbnailType.video, key: Key(video.id), videoId: video.id, width: width, diff --git a/lib/ui/dialogs/track_info_dialog.dart b/lib/ui/dialogs/track_info_dialog.dart index 04ebce4d..c3b3747f 100644 --- a/lib/ui/dialogs/track_info_dialog.dart +++ b/lib/ui/dialogs/track_info_dialog.dart @@ -70,7 +70,7 @@ Future showTrackInfoDialog( } final ap = AudioPlayer(); - await ap.setAudioSource(track.toAudioSource(0, 0)); + await ap.setAudioSource(track.toAudioSource(0, 0, null)); ap.play(); NamidaNavigator.inst.navigateDialog( diff --git a/lib/ui/pages/about_page.dart b/lib/ui/pages/about_page.dart index 89e5ec55..eb43c55f 100644 --- a/lib/ui/pages/about_page.dart +++ b/lib/ui/pages/about_page.dart @@ -465,30 +465,54 @@ class _NamidaMarkdownElementBuilderHeader extends MarkdownElementBuilder { } class _NamidaMarkdownElementBuilderCommitLink extends MarkdownElementBuilder { - String? shortenLongHash(String? longHash, {int chars = 5}) { - if (longHash == null || longHash == '') return null; - return longHash.substring(0, 7); - } + final regex = RegExp(r'([a-f0-9]{7}):', caseSensitive: false); @override Widget? visitText(md.Text text, TextStyle? preferredStyle) { - final regex = RegExp(r'([a-fA-F0-9]{40}):', caseSensitive: false); final res = regex.firstMatch(text.text); - final longHash = res?.group(1); - final url = "${AppSocial.GITHUB}/commit/$longHash"; - final textWithoutCommit = longHash == null ? text.text : text.text.replaceFirst(regex, ''); - final commit = shortenLongHash(longHash); + final shortHash = res?.group(1); + final url = "${AppSocial.GITHUB}/commit/$shortHash"; + final textWithoutCommit = shortHash == null ? text.text : text.text.substring(shortHash.length + 1); + return _CommitTapWidget( + url: url, + commit: shortHash, + textWithoutCommit: textWithoutCommit, + ); + } +} + +class _CommitTapWidget extends StatefulWidget { + final String url; + final String? commit; + final String textWithoutCommit; + const _CommitTapWidget({required this.url, required this.commit, required this.textWithoutCommit}); + + @override + State<_CommitTapWidget> createState() => _CommitTapWidgetState(); +} + +class _CommitTapWidgetState extends State<_CommitTapWidget> { + late final TapGestureRecognizer recognizer = TapGestureRecognizer()..onTap = () => NamidaLinkUtils.openLink(widget.url); + + @override + void dispose() { + recognizer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Text.rich( TextSpan( - text: commit == null ? '' : "#$commit:", + text: widget.commit == null ? '' : "#${widget.commit}:", style: namida.textTheme.displayMedium?.copyWith( fontSize: 13.5, color: namida.theme.colorScheme.secondary, ), - recognizer: TapGestureRecognizer()..onTap = () => NamidaLinkUtils.openLink(url), + recognizer: recognizer, children: [ TextSpan( - text: textWithoutCommit, + text: widget.textWithoutCommit, style: namida.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w400, fontSize: 13.0, diff --git a/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart b/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart index 7667a560..2f5ea3ee 100644 --- a/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart +++ b/lib/ui/pages/subpages/indexer_missing_tracks_subpage.dart @@ -491,17 +491,7 @@ class _IndexerMissingTracksSubpageState extends State with TickerPro if (item is YoutubeID) { final vidId = item.id; return YoutubeThumbnail( + type: ThumbnailType.video, key: Key(vidId), isImportantInCache: true, width: fallbackWidth, diff --git a/lib/youtube/controller/youtube_current_info.dart b/lib/youtube/controller/youtube_current_info.dart index 512b8bfa..b88701b2 100644 --- a/lib/youtube/controller/youtube_current_info.dart +++ b/lib/youtube/controller/youtube_current_info.dart @@ -8,6 +8,7 @@ class _YoutubeCurrentInfoController { RxBaseCore get currentVideoPage => _currentVideoPage; RxBaseCore get currentComments => _currentComments; + RxBaseCore get isLoadingVideoPage => _isLoadingVideoPage; RxBaseCore get isLoadingInitialComments => _isLoadingInitialComments; RxBaseCore get isLoadingMoreComments => _isLoadingMoreComments; RxBaseCore get currentFeed => _currentFeed; @@ -23,6 +24,7 @@ class _YoutubeCurrentInfoController { final _currentRelatedVideos = Rxn(); final _currentComments = Rxn(); final currentYTStreams = Rxn(); + final _isLoadingVideoPage = false.obs; final _isLoadingInitialComments = false.obs; final _isLoadingMoreComments = false.obs; final _isCurrentCommentsFromCache = Rxn(); @@ -44,6 +46,7 @@ class _YoutubeCurrentInfoController { _currentComments.value = null; currentYTStreams.value = null; _isLoadingInitialComments.value = false; + _isLoadingVideoPage.value = false; _isLoadingMoreComments.value = false; _isCurrentCommentsFromCache.value = null; } @@ -70,7 +73,7 @@ class _YoutubeCurrentInfoController { return comms != null; } - Future updateVideoPage(String videoId, {required bool forceRequestPage, required bool forceRequestComments, CommentsSortType? commentsSort}) async { + Future updateVideoPage(String videoId, {required bool requestPage, required bool requestComments, CommentsSortType? commentsSort}) async { if (!ConnectivityController.inst.hasConnection) { snackyy( title: lang.ERROR, @@ -80,23 +83,26 @@ class _YoutubeCurrentInfoController { ); return; } + if (!requestPage && !requestComments) return; - if (forceRequestPage) { + if (requestPage) { if (onVideoPageReset != null) onVideoPageReset!(); // jumps miniplayer to top _currentVideoPage.value = null; } - if (forceRequestComments) { + if (requestComments) { _currentComments.value = null; _initialCommentsContinuation = null; } commentsSort ??= YoutubeMiniplayerUiController.inst.currentCommentSort.value; - final page = await YoutubeInfoController.video.fetchVideoPage(videoId, details: forceRequestPage ? ExecuteDetails.forceRequest() : null); + _isLoadingVideoPage.value = true; + final page = await YoutubeInfoController.video.fetchVideoPage(videoId, details: ExecuteDetails.forceRequest()); + _isLoadingVideoPage.value = false; if (_canSafelyModifyMetadata(videoId)) { - _currentVideoPage.value = page; - if (forceRequestComments) { + if (requestPage) _currentVideoPage.value = page; // page is still requested cuz comments need it + if (requestComments) { final commentsContinuation = page?.commentResult.continuation; if (commentsContinuation != null && _canShowComments) { _isLoadingInitialComments.value = true; @@ -105,7 +111,7 @@ class _YoutubeCurrentInfoController { continuationToken: commentsContinuation, details: ExecuteDetails.forceRequest(), ); - if (identical(page, _currentVideoPage.value)) { + if (_canSafelyModifyMetadata(videoId)) { _isLoadingInitialComments.value = false; _currentVideoPage.refresh(); _currentComments.value = comm; diff --git a/lib/youtube/controller/youtube_local_search_controller.dart b/lib/youtube/controller/youtube_local_search_controller.dart index 63659ae9..b54b6a06 100644 --- a/lib/youtube/controller/youtube_local_search_controller.dart +++ b/lib/youtube/controller/youtube_local_search_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:youtipie/class/cache_details.dart'; import 'package:youtipie/class/publish_time.dart'; import 'package:youtipie/class/stream_info_item/stream_info_item.dart'; @@ -258,23 +257,27 @@ class YTLocalSearchController with PortsProvider { lookupListStreamInfoMapCacheDetails.loop( (db) { db.loadEverything((map) { - final id = map['id']; - if (id != null && lookupItemAvailable[id] == null) { - lookupListStreamInfoMap.add(map); - lookupItemAvailable[id] = (list: 2, index: lookupListStreamInfoMap.length - 1); - } + try { + final id = map['id']; + if (id != null && lookupItemAvailable[id] == null) { + lookupListStreamInfoMap.add(map); + lookupItemAvailable[id] = (list: 2, index: lookupListStreamInfoMap.length - 1); + } + } catch (_) {} }); }, ); lookupListVideoStreamsMapCacheDetails.loop( (db) { db.loadEverything((map) { - final info = map['info'] as Map; - final id = info['id']; - if (id != null && lookupItemAvailable[id] == null) { - lookupListVideoStreamsMap.add(info.cast()); - lookupItemAvailable[id] = (list: 3, index: lookupListVideoStreamsMap.length - 1); - } + try { + final info = map['info'] as Map; + final id = info['id']; + if (id != null && lookupItemAvailable[id] == null) { + lookupListVideoStreamsMap.add(info.cast()); + lookupItemAvailable[id] = (list: 3, index: lookupListVideoStreamsMap.length - 1); + } + } catch (_) {} }); }, ); @@ -345,7 +348,6 @@ class YTLocalSearchController with PortsProvider { extension _VideoInfoUtils on VideoStreamInfo { StreamInfoItem toStreamInfo() { final vid = this; - final date = vid.publishedAt.date; return StreamInfoItem( id: vid.id, title: vid.title, @@ -357,7 +359,7 @@ extension _VideoInfoUtils on VideoStreamInfo { thumbnails: [], ), thumbnailGifUrl: null, - publishedFromText: date == null ? '' : Jiffy.parseFromDateTime(date).fromNow(), + publishedFromText: '', // should never be used, use [publishedAt] instead. publishedAt: vid.publishedAt, indexInPlaylist: null, durSeconds: null, diff --git a/lib/youtube/controller/yt_generators_controller.dart b/lib/youtube/controller/yt_generators_controller.dart index dbfe3ef0..3dcc33ff 100644 --- a/lib/youtube/controller/yt_generators_controller.dart +++ b/lib/youtube/controller/yt_generators_controller.dart @@ -202,16 +202,18 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port (db) { db.loadEverything( (map) { - final id = map['id']; - if (id != null && releaseDateMap[id] == null) { - DateTime? date; - try { - date = PublishTime.fromMap(map['publishedAt']).date; - } catch (_) {} - allIds.add(id); - allIdsAdded[id] = true; - releaseDateMap[id] = date; - } + try { + final id = map['id']; + if (id != null && releaseDateMap[id] == null) { + DateTime? date; + try { + date = PublishTime.fromMap(map['publishedAt']).date; + } catch (_) {} + allIds.add(id); + allIdsAdded[id] = true; + releaseDateMap[id] = date; + } + } catch (_) {} }, ); }, @@ -219,22 +221,24 @@ class NamidaYTGenerator extends NamidaGeneratorBase with Port lookupListVideoStreamsMapCacheDetails.loop((db) { db.loadEverything( (map) { - final info = map['info'] as Map; - final id = info['id']; - if (id != null && releaseDateMap[id] == null) { - DateTime? date; - try { - date = PublishTime.fromMap(info['publishDate']).date; - } catch (_) {} - if (date == null) { + try { + final info = map['info'] as Map; + final id = info['id']; + if (id != null && releaseDateMap[id] == null) { + DateTime? date; try { - date = PublishTime.fromMap(info['uploadDate']).date; + date = PublishTime.fromMap(info['publishDate']).date; } catch (_) {} + if (date == null) { + try { + date = PublishTime.fromMap(info['uploadDate']).date; + } catch (_) {} + } + allIds.add(id); + allIdsAdded[id] = true; + releaseDateMap[id] = date; } - allIds.add(id); - allIdsAdded[id] = true; - releaseDateMap[id] = date; - } + } catch (_) {} }, ); }); diff --git a/lib/youtube/functions/download_sheet.dart b/lib/youtube/functions/download_sheet.dart index ca4097be..210ec1ca 100644 --- a/lib/youtube/functions/download_sheet.dart +++ b/lib/youtube/functions/download_sheet.dart @@ -297,6 +297,7 @@ Future showDownloadVideoBottomSheet({ child: Row( children: [ YoutubeThumbnail( + type: ThumbnailType.video, key: Key(videoId), isImportantInCache: true, borderRadius: 10.0, diff --git a/lib/youtube/pages/yt_channel_subpage.dart b/lib/youtube/pages/yt_channel_subpage.dart index 2135cca4..0e4d949c 100644 --- a/lib/youtube/pages/yt_channel_subpage.dart +++ b/lib/youtube/pages/yt_channel_subpage.dart @@ -5,9 +5,10 @@ import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; import 'package:youtipie/class/channels/channel_page_result.dart'; import 'package:youtipie/class/execute_details.dart'; -import 'package:youtipie/class/youtipie_feed/channel_info_item.dart'; import 'package:youtipie/class/stream_info_item/stream_info_item.dart'; +import 'package:youtipie/class/youtipie_feed/channel_info_item.dart'; +import 'package:namida/base/pull_to_refresh.dart'; import 'package:namida/base/youtube_channel_controller.dart'; import 'package:namida/class/route.dart'; import 'package:namida/controller/connectivity.dart'; @@ -43,7 +44,10 @@ class YTChannelSubpage extends StatefulWidget with NamidaRouteWidget { State createState() => _YTChannelSubpageState(); } -class _YTChannelSubpageState extends YoutubeChannelController { +class _YTChannelSubpageState extends YoutubeChannelController with TickerProviderStateMixin, PullToRefreshMixin { + @override + double get maxDistance => 64.0; + late final YoutubeSubscription ch = YoutubeSubscriptionsController.inst.availableChannels.value[widget.channelID] ?? YoutubeSubscription( channelID: widget.channelID.splitLast('/'), @@ -68,7 +72,7 @@ class _YTChannelSubpageState extends YoutubeChannelController (value) { if (value != null) { setState(() => _channelInfo = value); - fetchChannelStreams(value); + super.onRefresh(() => fetchChannelStreams(value, forceRequest: true), forceShow: true); } }, ); @@ -137,234 +141,245 @@ class _YTChannelSubpageState extends YoutubeChannelController const bannerHeight = 69.0; return BackgroundWrapper( - child: Column( - children: [ - Stack( - children: [ - if (bannerUrl != null) - TapDetector( - onTap: () => _onImageTap(context, channelID, bannerUrl, true), - child: NamidaHero( - tag: 'true_${channelID}_$bannerUrl', - child: YoutubeThumbnail( - key: Key('${channelID}_$bannerUrl'), - width: context.width, - compressed: false, - isImportantInCache: false, - customUrl: bannerUrl, - borderRadius: 0, - displayFallbackIcon: false, - height: bannerHeight, - ), - ), - ), - Padding( - padding: (bannerUrl == null ? EdgeInsets.zero : const EdgeInsets.only(top: bannerHeight * 0.95)), - child: Row( + child: Listener( + onPointerMove: (event) => onPointerMove(uploadsScrollController, event), + onPointerUp: (event) => _channelInfo == null ? null : onRefresh(() => fetchChannelStreams(_channelInfo!, forceRequest: true)), + onPointerCancel: (event) => onVerticalDragFinish(), + child: Stack( + alignment: Alignment.topCenter, + children: [ + Column( + children: [ + Stack( children: [ - const SizedBox(width: 12.0), - Transform.translate( - offset: bannerUrl == null ? const Offset(0, 0) : const Offset(0, -bannerHeight * 0.1), - child: TapDetector( - onTap: () => _onImageTap(context, channelID, avatarUrl, false), + if (bannerUrl != null) + TapDetector( + onTap: () => _onImageTap(context, channelID, bannerUrl, true), child: NamidaHero( - tag: 'false_${channelID}_$avatarUrl', + tag: 'true_${channelID}_$bannerUrl', child: YoutubeThumbnail( - key: Key('${channelID}_$avatarUrl'), - width: context.width * 0.14, - isImportantInCache: true, - customUrl: avatarUrl, - isCircle: true, + type: ThumbnailType.channel, + key: Key('${channelID}_$bannerUrl'), + width: context.width, compressed: false, + isImportantInCache: false, + customUrl: bannerUrl, + borderRadius: 0, + displayFallbackIcon: false, + height: bannerHeight, ), ), ), - ), - const SizedBox(width: 6.0), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Padding( + padding: (bannerUrl == null ? EdgeInsets.zero : const EdgeInsets.only(top: bannerHeight * 0.95)), + child: Row( children: [ - Padding( - padding: const EdgeInsets.only(left: 2.0), - child: Text( - _channelInfo?.title ?? ch.title, - style: context.textTheme.displayLarge, + const SizedBox(width: 12.0), + Transform.translate( + offset: bannerUrl == null ? const Offset(0, 0) : const Offset(0, -bannerHeight * 0.1), + child: TapDetector( + onTap: () => _onImageTap(context, channelID, avatarUrl, false), + child: NamidaHero( + tag: 'false_${channelID}_$avatarUrl', + child: YoutubeThumbnail( + type: ThumbnailType.channel, // banner akshully + key: Key('${channelID}_$avatarUrl'), + width: context.width * 0.14, + isImportantInCache: true, + customUrl: avatarUrl, + isCircle: true, + compressed: false, + ), + ), ), ), - const SizedBox(height: 4.0), - Text( - subsCountText ?? - (subsCount == null - ? '? ${lang.SUBSCRIBERS}' - : [ - subsCount.formatDecimalShort(), - subsCount < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, - ].join(' ')), - style: context.textTheme.displayMedium?.copyWith( - fontSize: 12.0, + const SizedBox(width: 6.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 2.0), + child: Text( + _channelInfo?.title ?? ch.title, + style: context.textTheme.displayLarge, + ), + ), + const SizedBox(height: 4.0), + Text( + subsCountText ?? + (subsCount == null + ? '? ${lang.SUBSCRIBERS}' + : [ + subsCount.formatDecimalShort(), + subsCount < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, + ].join(' ')), + style: context.textTheme.displayMedium?.copyWith( + fontSize: 12.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), + const SizedBox(width: 4.0), + YTSubscribeButton(channelID: channelID), + const SizedBox(width: 12.0), ], ), ), - const SizedBox(width: 4.0), - YTSubscribeButton(channelID: channelID), - const SizedBox(width: 12.0), ], ), - ), - ], - ), - const SizedBox(height: 4.0), - Row( - children: [ - const SizedBox(width: 4.0), - Expanded(child: sortWidget), - const SizedBox(width: 4.0), - Obx( - () => NamidaInkWellButton( - animationDurationMS: 100, - sizeMultiplier: 0.95, - borderRadius: 8.0, - icon: Broken.task_square, - text: lang.LOAD_ALL, - enabled: !isLoadingMoreUploads.valueR && !lastLoadingMoreWasEmpty.valueR, - disableWhenLoading: false, - showLoadingWhenDisabled: !lastLoadingMoreWasEmpty.valueR, - onTap: () async { - _canKeepLoadingMore = !_canKeepLoadingMore; - while (_canKeepLoadingMore && !lastLoadingMoreWasEmpty.value && ConnectivityController.inst.hasConnection) { - await fetchStreamsNextPage(); - } - }, - ), - ), - const SizedBox(width: 4.0), - ], - ), - const SizedBox(height: 10.0), - Row( - children: [ - const SizedBox(width: 8.0), - Expanded( - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: 4.0, + const SizedBox(height: 4.0), + Row( children: [ - NamidaInkWell( - borderRadius: 6.0, - decoration: BoxDecoration( - border: Border.all(color: context.theme.colorScheme.secondary.withOpacity(0.5)), + const SizedBox(width: 4.0), + Expanded(child: sortWidget), + const SizedBox(width: 4.0), + Obx( + () => NamidaInkWellButton( + animationDurationMS: 100, + sizeMultiplier: 0.95, + borderRadius: 8.0, + icon: Broken.task_square, + text: lang.LOAD_ALL, + enabled: !isLoadingMoreUploads.valueR && !lastLoadingMoreWasEmpty.valueR, + disableWhenLoading: false, + showLoadingWhenDisabled: !lastLoadingMoreWasEmpty.valueR, + onTap: () async { + _canKeepLoadingMore = !_canKeepLoadingMore; + while (_canKeepLoadingMore && !lastLoadingMoreWasEmpty.value && ConnectivityController.inst.hasConnection) { + await fetchStreamsNextPage(); + } + }, ), - padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 3.0), - child: Row( - mainAxisSize: MainAxisSize.min, + ), + const SizedBox(width: 4.0), + ], + ), + const SizedBox(height: 10.0), + Row( + children: [ + const SizedBox(width: 8.0), + Expanded( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: 4.0, children: [ - const Icon(Broken.video_square, size: 16.0), - const SizedBox(width: 4.0), - Text( - "${streamsList?.length ?? '?'} / ${streamsCount ?? '?'}", - style: context.textTheme.displayMedium, + NamidaInkWell( + borderRadius: 6.0, + decoration: BoxDecoration( + border: Border.all(color: context.theme.colorScheme.secondary.withOpacity(0.5)), + ), + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 3.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Broken.video_square, size: 16.0), + const SizedBox(width: 4.0), + Text( + "${streamsList?.length ?? '?'} / ${streamsCount ?? '?'}", + style: context.textTheme.displayMedium, + ), + ], + ), ), + const SizedBox(width: 4.0), + if (streamsPeakDates != null) + NamidaInkWell( + borderRadius: 6.0, + decoration: BoxDecoration( + border: Border.all(color: context.theme.colorScheme.secondary.withOpacity(0.5)), + ), + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), + child: Text( + "${streamsPeakDates!.oldest.millisecondsSinceEpoch.dateFormattedOriginal} (${Jiffy.parseFromDateTime(streamsPeakDates!.oldest).fromNow()})", + style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), + ), + ), ], ), ), const SizedBox(width: 4.0), - if (streamsPeakDates != null) - NamidaInkWell( - borderRadius: 6.0, - decoration: BoxDecoration( - border: Border.all(color: context.theme.colorScheme.secondary.withOpacity(0.5)), - ), - padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), - child: Text( - "${streamsPeakDates!.oldest.millisecondsSinceEpoch.dateFormattedOriginal} (${Jiffy.parseFromDateTime(streamsPeakDates!.oldest).fromNow()})", - style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), - ), - ), - ], - ), - ), - const SizedBox(width: 4.0), - YTVideosActionBar( - title: _channelInfo?.title ?? ch.title, - urlBuilder: _channelInfo?.buildUrl, - barOptions: const YTVideosActionBarOptions( - addToPlaylist: false, - playLast: false, - ), - videosCallback: () => streamsList - ?.map((e) => YoutubeID( - id: e.id, - playlistID: null, - )) - .toList(), - infoLookupCallback: () { - final streamsList = this.streamsList; - if (streamsList == null) return null; - final m = {}; - streamsList.loop((e) => m[e.id] = e); - return m; - }, - ), - const SizedBox(width: 8.0), - ], - ), - const SizedBox(height: 8.0), - Expanded( - child: NamidaScrollbar( - controller: uploadsScrollController, - child: isLoadingInitialStreams - ? ShimmerWrapper( - shimmerEnabled: true, - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: 15, - itemBuilder: (context, index) { - return const YoutubeVideoCardDummy( - shimmerEnabled: true, - thumbnailHeight: thumbnailHeight, - thumbnailWidth: thumbnailWidth, - thumbnailWidthPercentage: 0.8, - ); - }, + YTVideosActionBar( + title: _channelInfo?.title ?? ch.title, + urlBuilder: _channelInfo?.buildUrl, + barOptions: const YTVideosActionBarOptions( + addToPlaylist: false, + playLast: false, ), - ) - : LazyLoadListView( - scrollController: uploadsScrollController, - onReachingEnd: () async { - await fetchStreamsNextPage(); - }, - listview: (controller) { + videosCallback: () => streamsList + ?.map((e) => YoutubeID( + id: e.id, + playlistID: null, + )) + .toList(), + infoLookupCallback: () { final streamsList = this.streamsList; - if (streamsList == null || streamsList.isEmpty) return const SizedBox(); - return ListView.builder( - padding: EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotalR), - controller: controller, - itemExtent: thumbnailItemExtent, - itemCount: streamsList.length, - itemBuilder: (context, index) { - final item = streamsList[index]; - return YoutubeVideoCard( - key: Key(item.id), - thumbnailHeight: thumbnailHeight, - thumbnailWidth: thumbnailWidth, - isImageImportantInCache: false, - video: item, - playlistID: null, - thumbnailWidthPercentage: 0.8, - dateInsteadOfChannel: true, - ); - }, - ); + if (streamsList == null) return null; + final m = {}; + streamsList.loop((e) => m[e.id] = e); + return m; }, ), + const SizedBox(width: 8.0), + ], + ), + const SizedBox(height: 8.0), + Expanded( + child: NamidaScrollbar( + controller: uploadsScrollController, + child: isLoadingInitialStreams + ? ShimmerWrapper( + shimmerEnabled: true, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: 15, + itemBuilder: (context, index) { + return const YoutubeVideoCardDummy( + shimmerEnabled: true, + thumbnailHeight: thumbnailHeight, + thumbnailWidth: thumbnailWidth, + thumbnailWidthPercentage: 0.8, + ); + }, + ), + ) + : LazyLoadListView( + scrollController: uploadsScrollController, + onReachingEnd: fetchStreamsNextPage, + listview: (controller) { + final streamsList = this.streamsList; + if (streamsList == null || streamsList.isEmpty) return const SizedBox(); + return ListView.builder( + padding: EdgeInsets.only(bottom: Dimensions.inst.globalBottomPaddingTotalR), + controller: controller, + itemExtent: thumbnailItemExtent, + itemCount: streamsList.length, + itemBuilder: (context, index) { + final item = streamsList[index]; + return YoutubeVideoCard( + key: Key(item.id), + thumbnailHeight: thumbnailHeight, + thumbnailWidth: thumbnailWidth, + isImageImportantInCache: false, + video: item, + playlistID: null, + thumbnailWidthPercentage: 0.8, + dateInsteadOfChannel: true, + ); + }, + ); + }, + ), + ), + ), + ], ), - ), - ], + pullToRefreshWidget, + ], + ), ), ); } diff --git a/lib/youtube/pages/yt_channels_page.dart b/lib/youtube/pages/yt_channels_page.dart index 91a1d7ac..61acd968 100644 --- a/lib/youtube/pages/yt_channels_page.dart +++ b/lib/youtube/pages/yt_channels_page.dart @@ -296,6 +296,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController { Padding( padding: const EdgeInsets.all(4.0), child: YoutubeThumbnail( + type: ThumbnailType.video, key: Key(id), borderRadius: 8.0, width: thumWidth - 4.0, diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart index 210067e1..4862f8f7 100644 --- a/lib/youtube/pages/yt_playlist_subpage.dart +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -174,6 +174,7 @@ class _YTNormalPlaylistSubpageState extends State { child: Stack( children: [ YoutubeThumbnail( + type: ThumbnailType.playlist, key: Key("$firstID"), width: context.width, height: context.width * 9 / 16, @@ -186,7 +187,7 @@ class _YTNormalPlaylistSubpageState extends State { onColorReady: (color) async { if (color != null) { await Future.delayed(const Duration(milliseconds: 200)); // navigation delay - setState(() { + refreshState(() { bgColor = color.color; }); } @@ -206,6 +207,7 @@ class _YTNormalPlaylistSubpageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ YoutubeThumbnail( + type: ThumbnailType.playlist, key: Key("$firstID"), width: bigThumbWidth, height: (bigThumbWidth * 9 / 16), @@ -280,7 +282,7 @@ class _YTNormalPlaylistSubpageState extends State { onTap: () async { final newName = await playlist.showRenamePlaylistSheet(context: context, playlistName: playlistCurrentName); if (context.mounted) { - setState(() { + refreshState(() { playlistCurrentName = newName; }); } @@ -391,7 +393,7 @@ class _YTHostedPlaylistSubpageState extends State with Color? get sortChipBGColor => bgColor?.withOpacity(0.6); @override - void onSortChanged(void Function() fn) => setState(fn); + void onSortChanged(void Function() fn) => refreshState(fn); late final ScrollController controller; final _isLoadingMoreItems = false.obs; @@ -455,7 +457,7 @@ class _YTHostedPlaylistSubpageState extends State with trySortStreams(); _isLoadingMoreItems.value = false; - setState(() {}); + refreshState(); } @override @@ -498,6 +500,7 @@ class _YTHostedPlaylistSubpageState extends State with child: Stack( children: [ YoutubeThumbnail( + type: ThumbnailType.playlist, key: Key("$firstID"), width: context.width, height: context.width * 9 / 16, @@ -511,7 +514,7 @@ class _YTHostedPlaylistSubpageState extends State with onColorReady: (color) async { if (color != null) { await Future.delayed(const Duration(milliseconds: 200)); // navigation delay - setState(() { + refreshState(() { bgColor = color.color; }); } @@ -531,6 +534,7 @@ class _YTHostedPlaylistSubpageState extends State with crossAxisAlignment: CrossAxisAlignment.start, children: [ YoutubeThumbnail( + type: ThumbnailType.playlist, key: Key("$firstID"), width: bigThumbWidth, height: (bigThumbWidth * 9 / 16), diff --git a/lib/youtube/pages/yt_search_results_page.dart b/lib/youtube/pages/yt_search_results_page.dart index ebd2112b..5eead9ca 100644 --- a/lib/youtube/pages/yt_search_results_page.dart +++ b/lib/youtube/pages/yt_search_results_page.dart @@ -130,7 +130,7 @@ class YoutubeSearchResultsPageState extends State with return BackgroundWrapper( child: Navigator( key: NamidaNavigator.inst.ytLocalSearchNavigatorKey, - onPopPage: (route, result) => true, + onPopPage: (route, result) => false, requestFocus: false, pages: [ MaterialPage( @@ -158,7 +158,7 @@ class YoutubeSearchResultsPageState extends State with initialSearch: currentSearchText, onVideoTap: widget.onVideoTap, onPopping: (didChangeSort) { - if (didChangeSort) setState(() {}); + if (didChangeSort) refreshState(); }, ), maintainState: false, @@ -199,7 +199,6 @@ class YoutubeSearchResultsPageState extends State with thumbnailWidthPercentage: 0.6, thumbnailHeight: thumbnailHeightLocal, thumbnailWidth: thumbnailWidthLocal, - dateInsteadOfChannel: true, isImageImportantInCache: false, video: item, playlistID: null, @@ -314,15 +313,10 @@ class YoutubeSearchResultsPageState extends State with short: item as StreamInfoItemShort, playlistID: null, ), - const (PlaylistInfoItem) => - // (item as PlaylistInfoItem).isMix - // ? YoutubePlaylistCardMix( - // firstVideoID: firstItem.id, - // title: firstItem.title, - // subtitle: chunk.title, - // ) - // : - YoutubePlaylistCard( + const (PlaylistInfoItem) => YoutubePlaylistCard( + thumbnailHeight: thumbnailHeight, + thumbnailWidth: thumbnailWidth, + playOnTap: false, playlist: item as PlaylistInfoItem, subtitle: item.subtitle.isNotEmpty ? item.subtitle : item.initialVideos.firstOrNull?.title, ), diff --git a/lib/youtube/widgets/yt_card.dart b/lib/youtube/widgets/yt_card.dart index ab1679e3..264dbe4c 100644 --- a/lib/youtube/widgets/yt_card.dart +++ b/lib/youtube/widgets/yt_card.dart @@ -32,7 +32,7 @@ class YoutubeCard extends StatelessWidget { final double? thumbnailWidth; final double? thumbnailHeight; final double fontMultiplier; - final bool isPlaylist; + final ThumbnailType thumbnailType; const YoutubeCard({ super.key, @@ -61,7 +61,7 @@ class YoutubeCard extends StatelessWidget { this.thumbnailWidth, this.thumbnailHeight, this.fontMultiplier = 1.0, - this.isPlaylist = false, + required this.thumbnailType, }); @override @@ -103,7 +103,7 @@ class YoutubeCard extends StatelessWidget { smallBoxIcon: smallBoxIcon, extractColor: extractColor, isCircle: isCircle, - isPlaylist: isPlaylist, + type: thumbnailType, ), ), const SizedBox(width: 8.0), @@ -155,6 +155,7 @@ class YoutubeCard extends StatelessWidget { height: channelThumbSize, shimmerEnabled: shimmerEnabled && (channelThumbnailUrl == null || !displayChannelThumbnail), child: YoutubeThumbnail( + type: ThumbnailType.channel, key: Key("${channelThumbnailUrl}_$channelID"), isImportantInCache: false, customUrl: channelThumbnailUrl, diff --git a/lib/youtube/widgets/yt_channel_card.dart b/lib/youtube/widgets/yt_channel_card.dart index e92babaa..b17a74bd 100644 --- a/lib/youtube/widgets/yt_channel_card.dart +++ b/lib/youtube/widgets/yt_channel_card.dart @@ -55,6 +55,7 @@ class _YoutubeChannelCardState extends State { height: thumbnailSize, shimmerEnabled: shimmerEnabled, child: YoutubeThumbnail( + type: ThumbnailType.channel, key: Key("${avatarUrl}_${channel?.id}"), compressed: false, isImportantInCache: true, diff --git a/lib/youtube/widgets/yt_comment_card.dart b/lib/youtube/widgets/yt_comment_card.dart index 80a7e03d..77eb889c 100644 --- a/lib/youtube/widgets/yt_comment_card.dart +++ b/lib/youtube/widgets/yt_comment_card.dart @@ -71,6 +71,7 @@ class YTCommentCard extends StatelessWidget { isCircle: true, shimmerEnabled: uploaderAvatar == null, child: YoutubeThumbnail( + type: ThumbnailType.channel, key: Key(uploaderAvatar ?? ''), isImportantInCache: false, customUrl: uploaderAvatar, @@ -325,6 +326,7 @@ class YTCommentCardCompact extends StatelessWidget { isCircle: true, shimmerEnabled: uploaderAvatar == null, child: YoutubeThumbnail( + type: ThumbnailType.channel, key: Key(uploaderAvatar ?? ''), isImportantInCache: false, customUrl: uploaderAvatar, diff --git a/lib/youtube/widgets/yt_description_widget.dart b/lib/youtube/widgets/yt_description_widget.dart index 2c99af00..136b8f87 100644 --- a/lib/youtube/widgets/yt_description_widget.dart +++ b/lib/youtube/widgets/yt_description_widget.dart @@ -88,6 +88,7 @@ class YoutubeDescriptionWidgetManager { InlineSpan _styleWrapperToSpan(StylesWrapper sw, String? videoId, Color linkColor) { if (sw.attachementUrl != null) { _latestAttachment = YoutubeThumbnail( + type: ThumbnailType.other, key: Key(sw.attachementUrl ?? ''), width: 16.0, isImportantInCache: true, @@ -190,6 +191,7 @@ class YoutubeDescriptionWidgetManager { return WidgetSpan( child: child, style: textStyle, + alignment: PlaceholderAlignment.middle, ); } else { TapGestureRecognizer? recognizer; diff --git a/lib/youtube/widgets/yt_download_task_item_card.dart b/lib/youtube/widgets/yt_download_task_item_card.dart index 05e50488..d9260134 100644 --- a/lib/youtube/widgets/yt_download_task_item_card.dart +++ b/lib/youtube/widgets/yt_download_task_item_card.dart @@ -518,6 +518,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { children: [ const SizedBox(width: 4.0), YoutubeThumbnail( + type: ThumbnailType.video, key: Key(item.id), borderRadius: 8.0, isImportantInCache: true, diff --git a/lib/youtube/widgets/yt_history_video_card.dart b/lib/youtube/widgets/yt_history_video_card.dart index a096e349..449ade4a 100644 --- a/lib/youtube/widgets/yt_history_video_card.dart +++ b/lib/youtube/widgets/yt_history_video_card.dart @@ -136,6 +136,7 @@ class YTHistoryVideoCard extends StatelessWidget { children: [ Center( child: YoutubeThumbnail( + type: ThumbnailType.video, key: Key(video.id), borderRadius: 8.0, isImportantInCache: isImportantInCache, diff --git a/lib/youtube/widgets/yt_playlist_card.dart b/lib/youtube/widgets/yt_playlist_card.dart index 57f283ec..cfceddf6 100644 --- a/lib/youtube/widgets/yt_playlist_card.dart +++ b/lib/youtube/widgets/yt_playlist_card.dart @@ -1,9 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:namida/core/utils.dart'; -import 'package:namida/packages/three_arched_circle.dart'; -import 'package:namida/youtube/controller/youtube_info_controller.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'; @@ -16,10 +13,14 @@ 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/utils.dart'; +import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/functions/yt_playlist_utils.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/widgets/yt_card.dart'; +import 'package:namida/youtube/widgets/yt_thumbnail.dart'; /// Playlist info is fetched automatically after 3 seconds of being displayed, or after attempting an action. class YoutubePlaylistCard extends StatefulWidget { @@ -28,6 +29,7 @@ class YoutubePlaylistCard extends StatefulWidget { final double? thumbnailWidth; final double? thumbnailHeight; final bool playOnTap; + final String? playingId; const YoutubePlaylistCard({ super.key, @@ -36,6 +38,7 @@ class YoutubePlaylistCard extends StatefulWidget { this.thumbnailWidth, this.thumbnailHeight, this.playOnTap = false, + this.playingId, }); @override @@ -54,7 +57,7 @@ class _YoutubePlaylistCardState extends State { Future _fetchFunction({required bool forceRequest}) { final executeDetails = forceRequest ? ExecuteDetails.forceRequest() : ExecuteDetails.cache(CacheDecision.cacheOnly); if (widget.playlist.isMix) { - final videoId = firstVideoID; + final videoId = firstVideoID ?? widget.playingId; if (videoId == null) return Future.value(null); return YoutubeInfoController.playlist.getMixPlaylist( videoId: videoId, @@ -124,7 +127,7 @@ class _YoutubePlaylistCardState extends State { } final thumbnailUrl = playlist.thumbnails.pick()?.url; final firstVideoID = this.firstVideoID; - final goodVideoID = firstVideoID != ''; + final goodVideoID = firstVideoID != null && firstVideoID != ''; return NamidaPopupWrapper( openOnTap: false, openOnLongPress: true, @@ -132,7 +135,7 @@ class _YoutubePlaylistCardState extends State { child: YoutubeCard( thumbnailHeight: widget.thumbnailHeight, thumbnailWidth: widget.thumbnailWidth, - isPlaylist: true, + thumbnailType: ThumbnailType.playlist, isImageImportantInCache: false, extractColor: true, borderRadius: 12.0, diff --git a/lib/youtube/widgets/yt_thumbnail.dart b/lib/youtube/widgets/yt_thumbnail.dart index 5cfab4ca..ed6dc5ba 100644 --- a/lib/youtube/widgets/yt_thumbnail.dart +++ b/lib/youtube/widgets/yt_thumbnail.dart @@ -15,6 +15,20 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/ui/widgets/artwork.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +enum ThumbnailType { + video, + playlist, + channel, + other, +} + +const _typeToIcon = { + ThumbnailType.video: Broken.video, + ThumbnailType.playlist: Broken.music_library_2, + ThumbnailType.channel: Broken.user, + ThumbnailType.other: null, +}; + class YoutubeThumbnail extends StatefulWidget { final String? videoId; final String? customUrl; @@ -35,7 +49,7 @@ class YoutubeThumbnail extends StatefulWidget { final bool compressed; final bool isImportantInCache; final bool preferLowerRes; - final bool isPlaylist; + final ThumbnailType type; final double? iconSize; final List? boxShadow; final bool forceSquared; @@ -61,7 +75,7 @@ class YoutubeThumbnail extends StatefulWidget { this.compressed = true, required this.isImportantInCache, this.preferLowerRes = true, - this.isPlaylist = false, + required this.type, this.iconSize, this.boxShadow, this.forceSquared = true, @@ -192,11 +206,7 @@ class _YoutubeThumbnailState extends State with LoadingItemsDe width: widget.width, thumbnailSize: widget.width, boxShadow: widget.boxShadow, - icon: widget.isPlaylist - ? Broken.music_library_2 - : widget.customUrl != null - ? Broken.user - : Broken.video, + icon: _typeToIcon[widget.type] ?? Broken.musicnote, iconSize: widget.iconSize ?? (widget.customUrl != null ? null : widget.width * 0.3), forceSquared: widget.forceSquared, // cacheHeight: (widget.height?.round() ?? widget.width.round()) ~/ 1.2, diff --git a/lib/youtube/widgets/yt_video_card.dart b/lib/youtube/widgets/yt_video_card.dart index 5282b4e4..ae80455d 100644 --- a/lib/youtube/widgets/yt_video_card.dart +++ b/lib/youtube/widgets/yt_video_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; import 'package:playlist_manager/module/playlist_id.dart'; import 'package:youtipie/class/result_wrapper/playlist_result.dart'; import 'package:youtipie/class/result_wrapper/playlist_result_base.dart'; @@ -14,6 +15,7 @@ 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'; import 'package:namida/youtube/widgets/yt_card.dart'; +import 'package:namida/youtube/widgets/yt_thumbnail.dart'; import 'package:namida/youtube/yt_utils.dart'; class YoutubeVideoCard extends StatelessWidget { @@ -64,12 +66,14 @@ class YoutubeVideoCard extends StatelessWidget { viewsCountText = "${viewsCount.formatDecimalShort()} ${viewsCount == 0 ? lang.VIEW : lang.VIEWS}"; } - String publishedFromText = video.publishedFromText; + DateTime? publishedDate = video.publishedAt.date; + final uploadDateAgo = publishedDate == null ? null : Jiffy.parseFromDateTime(publishedDate).fromNow(); return NamidaPopupWrapper( openOnTap: false, childrenDefault: getMenuItems, child: YoutubeCard( + thumbnailType: ThumbnailType.video, thumbnailWidthPercentage: thumbnailWidthPercentage, fontMultiplier: fontMultiplier, thumbnailWidth: thumbnailWidth, @@ -82,10 +86,10 @@ class YoutubeVideoCard extends StatelessWidget { title: video.title, subtitle: [ if (viewsCountText != null && viewsCountText.isNotEmpty) viewsCountText, - if (publishedFromText.isNotEmpty) publishedFromText, + if (uploadDateAgo != null) uploadDateAgo, ].join(' - '), displaythirdLineText: true, - thirdLineText: dateInsteadOfChannel ? video.publishedAt.date?.millisecondsSinceEpoch.dateAndClockFormattedOriginal ?? '' : video.channel.title, + thirdLineText: dateInsteadOfChannel ? video.shortDescription ?? '' : video.channel.title, displayChannelThumbnail: !dateInsteadOfChannel, channelThumbnailUrl: video.channel.thumbnails.pick()?.url, onTap: onTap ?? @@ -151,6 +155,7 @@ class YoutubeShortVideoCard extends StatelessWidget { openOnTap: false, childrenDefault: getMenuItems, child: YoutubeCard( + thumbnailType: ThumbnailType.video, thumbnailWidthPercentage: thumbnailWidthPercentage, fontMultiplier: fontMultiplier, thumbnailWidth: thumbnailWidth, @@ -205,6 +210,7 @@ class YoutubeVideoCardDummy extends StatelessWidget { @override Widget build(BuildContext context) { return YoutubeCard( + thumbnailType: ThumbnailType.video, thumbnailWidthPercentage: thumbnailWidthPercentage, fontMultiplier: fontMultiplier, thumbnailWidth: thumbnailWidth, diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 3454aadb..ffab24a4 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -232,1014 +232,1016 @@ class YoutubeMiniPlayerState extends State { return ObxO( rx: YoutubeInfoController.current.currentYTStreams, builder: (streams) => ObxO( - rx: YoutubeInfoController.current.currentVideoPage, - builder: (page) { - final videoInfo = page?.videoInfo; - final videoInfoStream = streams?.info; - final channel = page?.channelInfo; + rx: YoutubeInfoController.current.isLoadingVideoPage, + builder: (isLoadingVideoPage) => ObxO( + rx: YoutubeInfoController.current.currentVideoPage, + builder: (page) { + final shimmerEnabled = isLoadingVideoPage && page == null; + final dummyVideoCard = YoutubeVideoCardDummy( + shimmerEnabled: shimmerEnabled, + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + displaythirdLineText: false, + ); + final videoInfo = page?.videoInfo; + final videoInfoStream = streams?.info; + final channel = page?.channelInfo; - String? uploadDate; - String? uploadDateAgo; + String? uploadDate; + String? uploadDateAgo; - final parsedDate = videoInfo?.publishedAt.date ?? videoInfoStream?.publishedAt.date ?? videoInfoStream?.publishDate.date; + final parsedDate = videoInfo?.publishedAt.date ?? videoInfoStream?.publishedAt.date ?? videoInfoStream?.publishDate.date; - if (parsedDate != null) { - uploadDate = parsedDate.millisecondsSinceEpoch.dateFormattedOriginal; - uploadDateAgo = Jiffy.parseFromDateTime(parsedDate).fromNow(); - } else { - uploadDateAgo = videoInfo?.publishedFromText; - } - final videoTitle = videoInfo?.title ?? videoInfoStream?.title; - final channelName = channel?.title ?? videoInfoStream?.channelName; + if (parsedDate != null) { + uploadDate = parsedDate.millisecondsSinceEpoch.dateFormattedOriginal; + uploadDateAgo = Jiffy.parseFromDateTime(parsedDate).fromNow(); + } else { + // uploadDateAgo = videoInfo?.publishedFromText; // warcrime + } + final videoTitle = videoInfo?.title ?? videoInfoStream?.title; + final channelName = channel?.title ?? videoInfoStream?.channelName; - final channelThumbnail = channel?.thumbnails.pick()?.url; - final channelIsVerified = channel?.isVerified ?? false; - final channelSubs = channel?.subscribersCount; - final channelID = channel?.id ?? videoInfoStream?.channelId; + final channelThumbnail = channel?.thumbnails.pick()?.url; + final channelIsVerified = channel?.isVerified ?? false; + final channelSubs = channel?.subscribersCount; + final channelID = channel?.id ?? videoInfoStream?.channelId; - final videoLikeCount = (isUserLiked ? 1 : 0) + (videoInfo?.engagement?.likesCount ?? 0); - const int? videoDislikeCount = null; - final videoViewCount = videoInfo?.viewsCount; + final videoLikeCount = (isUserLiked ? 1 : 0) + (videoInfo?.engagement?.likesCount ?? 0); + const int? videoDislikeCount = null; + final videoViewCount = videoInfo?.viewsCount; - final description = videoInfo?.description; - final descriptionWidget = description == null - ? null - : YoutubeDescriptionWidget( - videoId: currentId, - content: description, - ); + final description = videoInfo?.description; + final descriptionWidget = description == null + ? null + : YoutubeDescriptionWidget( + videoId: currentId, + content: description, + ); - YoutubeController.inst.downloadedFilesMap; // for refreshing. - final downloadedFileExists = YoutubeController.inst.doesIDHasFileDownloaded(currentId) != null; + YoutubeController.inst.downloadedFilesMap; // for refreshing. + final downloadedFileExists = YoutubeController.inst.doesIDHasFileDownloaded(currentId) != null; - final defaultIconColor = context.defaultIconColor(CurrentColor.inst.miniplayerColor); + final defaultIconColor = context.defaultIconColor(CurrentColor.inst.miniplayerColor); - // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== - final miniplayerBody = Stack( - alignment: Alignment.bottomCenter, // bottom alignment is for touch absorber - children: [ - // opacity: (percentage * 4 - 3).withMinimum(0), - Listener( - key: Key("${currentId}_body_listener"), - onPointerMove: (event) { - if (event.delta.dy > 0) { - if (_scrollController.hasClients) { - if (_scrollController.position.pixels <= 0) { - _updateCanScrollQueue(false); + // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== + final miniplayerBody = Stack( + alignment: Alignment.bottomCenter, // bottom alignment is for touch absorber + children: [ + // opacity: (percentage * 4 - 3).withMinimum(0), + Listener( + key: Key("${currentId}_body_listener"), + onPointerMove: (event) { + if (event.delta.dy > 0) { + if (_scrollController.hasClients) { + if (_scrollController.position.pixels <= 0) { + _updateCanScrollQueue(false); + } } + } else { + if (_mpState == null || _mpState?.controller.value == 1) _updateCanScrollQueue(true); } - } else { - if (_mpState == null || _mpState?.controller.value == 1) _updateCanScrollQueue(true); - } - }, - onPointerDown: (_) { - cancelDimTimer(); - _updateCanScrollQueue(true); - }, - onPointerUp: (_) { - startDimTimer(); - _updateCanScrollQueue(true); - }, - child: Navigator( - key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, - requestFocus: false, - onPopPage: (route, result) => false, - restorationScopeId: currentId, - pages: [ - MaterialPage( - maintainState: true, - child: IgnorePointer( - ignoring: !_canScrollQueue, - child: LazyLoadListView( - key: Key("${currentId}_body_lazy_load_list"), - onReachingEnd: () async { - if (settings.ytTopComments.value) return; - await YoutubeInfoController.current.updateCurrentComments(currentId); - }, - extend: 400, - scrollController: _scrollController, - listview: (controller) => Stack( - key: Key("${currentId}_body_stack"), - children: [ - CustomScrollView( - // key: PageStorageKey(currentId), // duplicate errors - physics: _canScrollQueue ? const ClampingScrollPhysicsModified() : const NeverScrollableScrollPhysics(), - controller: controller, - slivers: [ - // --START-- title & subtitle - SliverToBoxAdapter( - key: Key("${currentId}_title"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: page == null, - child: ExpansionTile( - // key: Key(currentId), - initiallyExpanded: false, - maintainState: false, - expandedAlignment: Alignment.centerLeft, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - tilePadding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 14.0), - textColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), mainTheme.colorScheme.onSurface), - collapsedTextColor: mainTheme.colorScheme.onSurface, - iconColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), mainTheme.colorScheme.onSurface), - collapsedIconColor: mainTheme.colorScheme.onSurface, - childrenPadding: const EdgeInsets.all(18.0), - onExpansionChanged: (value) => _isTitleExpanded.value = value, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () { - final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; - if (videoListens.isEmpty) return const SizedBox(); - return NamidaInkWell( - borderRadius: 6.0, - bgColor: CurrentColor.inst.miniplayerColor.withOpacity(0.7), - onTap: () { - showVideoListensDialog(currentId); - }, - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), - child: Text( - videoListens.length.formatDecimal(), - style: mainTextTheme.displaySmall?.copyWith( - color: Colors.white.withOpacity(0.6), - ), - ), - ); - }, - ), - const SizedBox(width: 8.0), - NamidaPopupWrapper( - onPop: () { - _numberOfRepeats.value = 1; - }, - childrenDefault: () { - final videoId = currentId; - final items = YTUtils.getVideoCardMenuItems( - videoId: videoId, - url: videoInfo?.buildUrl(), - channelID: channelID, - playlistID: null, - idsNamesLookup: {videoId: videoTitle}, - ); - if (Player.inst.currentVideo != null && videoId == Player.inst.currentVideo?.id) { - final repeatForWidget = NamidaPopupItem( - icon: Broken.cd, - title: '', - titleBuilder: (style) => Obx( - () => Text( - lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', _numberOfRepeats.valueR.toString()), - style: style, - ), - ), + }, + onPointerDown: (_) { + cancelDimTimer(); + _updateCanScrollQueue(true); + }, + onPointerUp: (_) { + startDimTimer(); + _updateCanScrollQueue(true); + }, + child: Navigator( + key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, + requestFocus: false, + onPopPage: (route, result) => false, + restorationScopeId: currentId, + pages: [ + MaterialPage( + maintainState: true, + child: IgnorePointer( + ignoring: !_canScrollQueue, + child: LazyLoadListView( + key: Key("${currentId}_body_lazy_load_list"), + onReachingEnd: () async { + if (settings.ytTopComments.value) return; + await YoutubeInfoController.current.updateCurrentComments(currentId); + }, + extend: 400, + scrollController: _scrollController, + listview: (controller) => Stack( + key: Key("${currentId}_body_stack"), + children: [ + CustomScrollView( + // key: PageStorageKey(currentId), // duplicate errors + physics: _canScrollQueue ? const ClampingScrollPhysicsModified() : const NeverScrollableScrollPhysics(), + controller: controller, + slivers: [ + // --START-- title & subtitle + SliverToBoxAdapter( + key: Key("${currentId}_title"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: shimmerEnabled, + child: ExpansionTile( + // key: Key(currentId), + initiallyExpanded: false, + maintainState: false, + expandedAlignment: Alignment.centerLeft, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 14.0), + textColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), mainTheme.colorScheme.onSurface), + collapsedTextColor: mainTheme.colorScheme.onSurface, + iconColor: Color.alphaBlend(CurrentColor.inst.miniplayerColor.withAlpha(40), mainTheme.colorScheme.onSurface), + collapsedIconColor: mainTheme.colorScheme.onSurface, + childrenPadding: const EdgeInsets.all(18.0), + onExpansionChanged: (value) => _isTitleExpanded.value = value, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () { + final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; + if (videoListens.isEmpty) return const SizedBox(); + return NamidaInkWell( + borderRadius: 6.0, + bgColor: CurrentColor.inst.miniplayerColor.withOpacity(0.7), onTap: () { - settings.player.save(repeatMode: RepeatMode.forNtimes); - Player.inst.updateNumberOfRepeats(_numberOfRepeats.value); + showVideoListensDialog(currentId); }, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - NamidaIconButton( - icon: Broken.minus_cirlce, - onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value - 1).clamp(1, 20), - iconSize: 20.0, - ), - NamidaIconButton( - icon: Broken.add_circle, - onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value + 1).clamp(1, 20), - iconSize: 20.0, + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), + child: Text( + videoListens.length.formatDecimal(), + style: mainTextTheme.displaySmall?.copyWith( + color: Colors.white.withOpacity(0.6), + ), + ), + ); + }, + ), + const SizedBox(width: 8.0), + NamidaPopupWrapper( + onPop: () { + _numberOfRepeats.value = 1; + }, + childrenDefault: () { + final videoId = currentId; + final items = YTUtils.getVideoCardMenuItems( + videoId: videoId, + url: videoInfo?.buildUrl(), + channelID: channelID, + playlistID: null, + idsNamesLookup: {videoId: videoTitle}, + ); + if (Player.inst.currentVideo != null && videoId == Player.inst.currentVideo?.id) { + final repeatForWidget = NamidaPopupItem( + icon: Broken.cd, + title: '', + titleBuilder: (style) => Obx( + () => Text( + lang.REPEAT_FOR_N_TIMES.replaceFirst('_NUM_', _numberOfRepeats.valueR.toString()), + style: style, ), - ], + ), + onTap: () { + settings.player.save(repeatMode: RepeatMode.forNtimes); + Player.inst.updateNumberOfRepeats(_numberOfRepeats.value); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + NamidaIconButton( + icon: Broken.minus_cirlce, + onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value - 1).clamp(1, 20), + iconSize: 20.0, + ), + NamidaIconButton( + icon: Broken.add_circle, + onPressed: () => _numberOfRepeats.value = (_numberOfRepeats.value + 1).clamp(1, 20), + iconSize: 20.0, + ), + ], + ), + ); + items.add(repeatForWidget); + } + items.add( + NamidaPopupItem( + icon: Broken.trash, + title: lang.CLEAR, + onTap: () { + YTUtils().showVideoClearDialog(context, videoId, CurrentColor.inst.miniplayerColor); + }, ), ); - items.add(repeatForWidget); - } - items.add( - NamidaPopupItem( - icon: Broken.trash, - title: lang.CLEAR, - onTap: () { - YTUtils().showVideoClearDialog(context, videoId, CurrentColor.inst.miniplayerColor); - }, - ), - ); - return items; - }, - child: const Icon( - Broken.arrow_down_2, - size: 20.0, + return items; + }, + child: const Icon( + Broken.arrow_down_2, + size: 20.0, + ), ), - ), - ], - ), - title: ObxO( - rx: _isTitleExpanded, - builder: (isTitleExpanded) { - String? dateToShow; - if (isTitleExpanded) { - dateToShow = uploadDate ?? uploadDateAgo; - } else { - dateToShow = uploadDateAgo ?? uploadDate; - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - width: maxWidth * 0.8, - height: 24.0, - borderRadius: 6.0, - shimmerEnabled: page == null, - child: Text( - videoTitle ?? '', - maxLines: isTitleExpanded ? 6 : 2, - overflow: TextOverflow.ellipsis, - style: mainTextTheme.displayLarge, + ], + ), + title: ObxO( + rx: _isTitleExpanded, + builder: (isTitleExpanded) { + String? dateToShow; + if (isTitleExpanded) { + dateToShow = uploadDate ?? uploadDateAgo; + } else { + dateToShow = uploadDateAgo ?? uploadDate; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NamidaDummyContainer( + width: maxWidth * 0.8, + height: 24.0, + borderRadius: 6.0, + shimmerEnabled: shimmerEnabled, + child: Text( + videoTitle ?? '', + maxLines: isTitleExpanded ? 6 : 2, + overflow: TextOverflow.ellipsis, + style: mainTextTheme.displayLarge, + ), ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - width: maxWidth * 0.7, - height: 12.0, - shimmerEnabled: page == null, - child: Text( - [ - if (videoViewCount != null) - "${videoViewCount.formatDecimalShort(isTitleExpanded)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", - if (dateToShow != null) dateToShow, - ].join(' • '), - style: mainTextTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), + const SizedBox(height: 4.0), + NamidaDummyContainer( + width: maxWidth * 0.7, + height: 12.0, + shimmerEnabled: shimmerEnabled, + child: Text( + [ + if (videoViewCount != null) + "${videoViewCount.formatDecimalShort(isTitleExpanded)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", + if (dateToShow != null) dateToShow, + ].join(' • '), + style: mainTextTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), + ), ), - ), - ], - ); - }, + ], + ); + }, + ), + children: [ + if (descriptionWidget != null) descriptionWidget, + ], ), - children: [ - if (descriptionWidget != null) descriptionWidget, - ], ), ), - ), - // --END-- title & subtitle + // --END-- title & subtitle - // --START-- buttons - SliverToBoxAdapter( - key: Key("${currentId}_buttons"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: page == null, - child: SizedBox( - width: maxWidth, - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - children: [ - const SizedBox(width: 4.0), - ObxO( - rx: _isTitleExpanded, - builder: (isTitleExpanded) => SmallYTActionButton( - title: page == null - ? null - : videoLikeCount < 1 - ? lang.LIKE - : videoLikeCount.formatDecimalShort(isTitleExpanded), - icon: Broken.like_1, - smallIconWidget: FittedBox( - child: NamidaRawLikeButton( - likedIcon: Broken.like_filled, - normalIcon: Broken.like_1, - disabledColor: mainTheme.iconTheme.color, - isLiked: isUserLiked, - onTap: (isLiked) async { - YoutubePlaylistController.inst.favouriteButtonOnPressed(currentId); - }, + // --START-- buttons + SliverToBoxAdapter( + key: Key("${currentId}_buttons"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: shimmerEnabled, + child: SizedBox( + width: maxWidth, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + const SizedBox(width: 4.0), + ObxO( + rx: _isTitleExpanded, + builder: (isTitleExpanded) => SmallYTActionButton( + title: shimmerEnabled + ? null + : videoLikeCount < 1 + ? lang.LIKE + : videoLikeCount.formatDecimalShort(isTitleExpanded), + icon: Broken.like_1, + smallIconWidget: FittedBox( + child: NamidaRawLikeButton( + likedIcon: Broken.like_filled, + normalIcon: Broken.like_1, + disabledColor: mainTheme.iconTheme.color, + isLiked: isUserLiked, + onTap: (isLiked) async { + YoutubePlaylistController.inst.favouriteButtonOnPressed(currentId); + }, + ), ), ), ), - ), - const SizedBox(width: 4.0), - ObxO( - rx: _isTitleExpanded, - builder: (isTitleExpanded) => SmallYTActionButton( - title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded) ?? '?', - icon: Broken.dislike, - onPressed: () {}, + const SizedBox(width: 4.0), + ObxO( + rx: _isTitleExpanded, + builder: (isTitleExpanded) => SmallYTActionButton( + title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded) ?? '?', + icon: Broken.dislike, + onPressed: () {}, + ), ), - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SHARE, - icon: Broken.share, - onPressed: () { - final url = videoInfo?.buildUrl(); - if (url != null) Share.share(url); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.REFRESH, - icon: Broken.refresh, - onPressed: () async => await YoutubeInfoController.current.updateVideoPage( - currentId, - forceRequestPage: true, - forceRequestComments: true, + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.SHARE, + icon: Broken.share, + onPressed: () { + final url = videoInfo?.buildUrl(); + if (url != null) Share.share(url); + }, ), - ), - const SizedBox(width: 4.0), - Obx( - () { - final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; - final audioPercText = audioProgress?.percentageText(prefix: lang.AUDIO); - final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; - final videoPercText = videoProgress?.percentageText(prefix: lang.VIDEO); + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.REFRESH, + icon: Broken.refresh, + onPressed: () async => await YoutubeInfoController.current.updateVideoPage( + currentId, + requestPage: true, + requestComments: true, + ), + ), + const SizedBox(width: 4.0), + Obx( + () { + final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; + final audioPercText = audioProgress?.percentageText(prefix: lang.AUDIO); + final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; + final videoPercText = videoProgress?.percentageText(prefix: lang.VIDEO); - final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; + final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; - final wasDownloading = videoProgress != null || audioProgress != null; - final icon = (wasDownloading && !isDownloading) - ? Broken.play_circle - : wasDownloading - ? Broken.pause_circle - : downloadedFileExists - ? Broken.tick_circle - : Broken.import; - return SmallYTActionButton( - titleWidget: videoPercText == null && audioPercText == null && isDownloading ? const LoadingIndicator() : null, - title: videoPercText ?? audioPercText ?? lang.DOWNLOAD, - icon: icon, - onLongPress: () async => await showDownloadVideoBottomSheet(videoId: currentId), - onPressed: () async { - if (isDownloading) { - YoutubeController.inst.pauseDownloadTask( - itemsConfig: [], - videosIds: [currentId], - groupName: '', - ); - } else if (wasDownloading) { - YoutubeController.inst.resumeDownloadTaskForIDs( - videosIds: [currentId], - groupName: '', - ); - } else { - await showDownloadVideoBottomSheet(videoId: currentId); - } - }, - ); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SAVE, - icon: Broken.music_playlist, - onPressed: () => showAddToPlaylistSheet( - ids: [currentId], - idsNamesLookup: { - currentId: videoTitle ?? '', + final wasDownloading = videoProgress != null || audioProgress != null; + final icon = (wasDownloading && !isDownloading) + ? Broken.play_circle + : wasDownloading + ? Broken.pause_circle + : downloadedFileExists + ? Broken.tick_circle + : Broken.import; + return SmallYTActionButton( + titleWidget: videoPercText == null && audioPercText == null && isDownloading ? const LoadingIndicator() : null, + title: videoPercText ?? audioPercText ?? lang.DOWNLOAD, + icon: icon, + onLongPress: () async => await showDownloadVideoBottomSheet(videoId: currentId), + onPressed: () async { + if (isDownloading) { + YoutubeController.inst.pauseDownloadTask( + itemsConfig: [], + videosIds: [currentId], + groupName: '', + ); + } else if (wasDownloading) { + YoutubeController.inst.resumeDownloadTaskForIDs( + videosIds: [currentId], + groupName: '', + ); + } else { + await showDownloadVideoBottomSheet(videoId: currentId); + } + }, + ); }, ), - ), - const SizedBox(width: 4.0), - ], + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.SAVE, + icon: Broken.music_playlist, + onPressed: () => showAddToPlaylistSheet( + ids: [currentId], + idsNamesLookup: { + currentId: videoTitle ?? '', + }, + ), + ), + const SizedBox(width: 4.0), + ], + ), ), ), ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 24.0)), - // --END- buttons + const SliverPadding(padding: EdgeInsets.only(top: 24.0)), + // --END- buttons - // --START- channel - SliverToBoxAdapter( - key: Key("${currentId}_channel"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: channelName == null || channelThumbnail == null || channelSubs == null, - child: Material( - type: MaterialType.transparency, - child: InkWell( - onTap: () { - final ch = channel ?? YoutubeInfoController.current.currentVideoPage.value?.channelInfo; - final chid = ch?.id; - if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: ch)); - }, - child: Row( - children: [ - const SizedBox(width: 18.0), - NamidaDummyContainer( - width: 42.0, - height: 42.0, - borderRadius: 100.0, - shimmerEnabled: channelThumbnail == null && (channelID == null || channelID.isEmpty), - child: YoutubeThumbnail( - key: Key("${channelThumbnail}_$channelID"), - isImportantInCache: true, - customUrl: channelThumbnail, + // --START- channel + SliverToBoxAdapter( + key: Key("${currentId}_channel"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: shimmerEnabled && (channelName == null || channelThumbnail == null || channelSubs == null), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () { + final ch = channel ?? YoutubeInfoController.current.currentVideoPage.value?.channelInfo; + final chid = ch?.id; + if (chid != null) NamidaNavigator.inst.navigateTo(YTChannelSubpage(channelID: chid, channel: ch)); + }, + child: Row( + children: [ + const SizedBox(width: 18.0), + NamidaDummyContainer( width: 42.0, height: 42.0, - isCircle: true, + borderRadius: 100.0, + shimmerEnabled: shimmerEnabled && channelThumbnail == null && (channelID == null || channelID.isEmpty), + child: YoutubeThumbnail( + type: ThumbnailType.channel, + key: Key("${channelThumbnail}_$channelID"), + isImportantInCache: true, + customUrl: channelThumbnail, + width: 42.0, + height: 42.0, + isCircle: true, + ), ), - ), - const SizedBox(width: 8.0), - Expanded( - // key: Key(currentId), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - child: Row( - children: [ - NamidaDummyContainer( - width: 114.0, - height: 12.0, - borderRadius: 4.0, - shimmerEnabled: channelName == null, - child: Text( - channelName ?? '', - style: mainTextTheme.displayMedium?.copyWith( - fontSize: 13.5, + const SizedBox(width: 8.0), + Expanded( + // key: Key(currentId), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Row( + children: [ + NamidaDummyContainer( + width: 114.0, + height: 12.0, + borderRadius: 4.0, + shimmerEnabled: shimmerEnabled && channelName == null, + child: Text( + channelName ?? '', + style: mainTextTheme.displayMedium?.copyWith( + fontSize: 13.5, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - ), - ), - if (channelIsVerified) ...[ - const SizedBox(width: 4.0), - const Icon( - Broken.shield_tick, - size: 12.0, ), - ] - ], + if (channelIsVerified) ...[ + const SizedBox(width: 4.0), + const Icon( + Broken.shield_tick, + size: 12.0, + ), + ] + ], + ), ), - ), - const SizedBox(height: 2.0), - FittedBox( - child: NamidaDummyContainer( - width: 92.0, - height: 10.0, - borderRadius: 4.0, - shimmerEnabled: channelSubs == null, - child: ObxO( - rx: _isTitleExpanded, - builder: (isTitleExpanded) => Text( - channelSubs == null - ? '? ${lang.SUBSCRIBERS}' - : [ - channelSubs.formatDecimalShort(isTitleExpanded), - channelSubs < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, - ].join(' '), - style: mainTextTheme.displaySmall?.copyWith( - fontSize: 12.0, + const SizedBox(height: 2.0), + FittedBox( + child: NamidaDummyContainer( + width: 92.0, + height: 10.0, + borderRadius: 4.0, + shimmerEnabled: shimmerEnabled && channelSubs == null, + child: ObxO( + rx: _isTitleExpanded, + builder: (isTitleExpanded) => Text( + channelSubs == null + ? '? ${lang.SUBSCRIBERS}' + : [ + channelSubs.formatDecimalShort(isTitleExpanded), + channelSubs < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS, + ].join(' '), + style: mainTextTheme.displaySmall?.copyWith( + fontSize: 12.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), ), - ), - ], + ], + ), ), - ), - const SizedBox(width: 12.0), - YTSubscribeButton(channelID: channelID), - const SizedBox(width: 20.0), - ], + const SizedBox(width: 12.0), + YTSubscribeButton(channelID: channelID), + const SizedBox(width: 20.0), + ], + ), ), ), ), ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - // --END-- channel + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + // --END-- channel - // --SRART-- top comments - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + // --SRART-- top comments + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - ObxO( - rx: settings.ytTopComments, - builder: (ytTopComments) { - if (!ytTopComments) return const SliverToBoxAdapter(); - return SliverToBoxAdapter( - child: ObxO( - rx: YoutubeInfoController.current.currentComments, - builder: (comments) => comments == null || comments.isEmpty - ? const SizedBox() - : NamidaInkWell( - key: Key("${currentId}_top_comments_highlight"), - bgColor: Color.alphaBlend(mainTheme.scaffoldBackgroundColor.withOpacity(0.4), mainTheme.cardColor), - margin: const EdgeInsets.symmetric(horizontal: 18.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - onTap: () { - NamidaNavigator.inst.isInYTCommentsSubpage = true; - NamidaNavigator.inst.ytMiniplayerCommentsPageKey.currentState?.pushPage( - const YTMiniplayerCommentsSubpage(), - maintainState: false, - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon( - Broken.document, - size: 16.0, - ), - const SizedBox(width: 8.0), - Expanded( - child: Text( - [ - lang.COMMENTS, - if (comments.commentsCount != null) comments.commentsCount!.formatDecimalShort(), - ].join(' • '), - style: mainTextTheme.displaySmall, - textAlign: TextAlign.start, + ObxO( + rx: settings.ytTopComments, + builder: (ytTopComments) { + if (!ytTopComments) return const SliverToBoxAdapter(); + return SliverToBoxAdapter( + child: ObxO( + rx: YoutubeInfoController.current.currentComments, + builder: (comments) => comments == null || comments.isEmpty + ? const SizedBox() + : NamidaInkWell( + key: Key("${currentId}_top_comments_highlight"), + bgColor: Color.alphaBlend(mainTheme.scaffoldBackgroundColor.withOpacity(0.4), mainTheme.cardColor), + margin: const EdgeInsets.symmetric(horizontal: 18.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + onTap: () { + NamidaNavigator.inst.isInYTCommentsSubpage = true; + NamidaNavigator.inst.ytMiniplayerCommentsPageKey.currentState?.pushPage( + const YTMiniplayerCommentsSubpage(), + maintainState: false, + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon( + Broken.document, + size: 16.0, ), - ), - ObxO( - rx: YoutubeInfoController.current.isCurrentCommentsFromCache, - builder: (commFromCache) { - commFromCache ??= false; - return NamidaIconButton( - horizontalPadding: 0.0, - 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 - ? StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - iconSize: 20.0, - secondaryIconSize: 12.0, - baseIconColor: defaultIconColor, - secondaryIconColor: defaultIconColor, - ) - : Icon( - Broken.refresh, - color: defaultIconColor, - size: 20.0, - ), - ); - }, - ), - ], - ), - const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), - ObxO( - rx: YoutubeInfoController.current.isLoadingInitialComments, - builder: (loading) => ShimmerWrapper( - shimmerEnabled: loading, - child: YTCommentCardCompact(comment: loading ? null : comments.items.firstOrNull), + const SizedBox(width: 8.0), + Expanded( + child: Text( + [ + lang.COMMENTS, + if (comments.commentsCount != null) comments.commentsCount!.formatDecimalShort(), + ].join(' • '), + style: mainTextTheme.displaySmall, + textAlign: TextAlign.start, + ), + ), + ObxO( + rx: YoutubeInfoController.current.isCurrentCommentsFromCache, + builder: (commFromCache) { + commFromCache ??= false; + return NamidaIconButton( + horizontalPadding: 0.0, + 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 + ? StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + iconSize: 20.0, + secondaryIconSize: 12.0, + baseIconColor: defaultIconColor, + secondaryIconColor: defaultIconColor, + ) + : Icon( + Broken.refresh, + color: defaultIconColor, + size: 20.0, + ), + ); + }, + ), + ], ), - ) - ], + const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), + ObxO( + rx: YoutubeInfoController.current.isLoadingInitialComments, + builder: (loading) => ShimmerWrapper( + shimmerEnabled: loading, + child: YTCommentCardCompact(comment: loading ? null : comments.items.firstOrNull), + ), + ) + ], + ), ), - ), - ), - ); - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 8.0)), + ), + ); + }, + ), + const SliverPadding(padding: EdgeInsets.only(top: 8.0)), - page == null - ? SliverToBoxAdapter( - key: Key("${currentId}_feed_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - padding: EdgeInsets.zero, - key: Key("${currentId}_feedlist_shimmer"), - physics: const NeverScrollableScrollPhysics(), - itemCount: 15, - shrinkWrap: true, - itemBuilder: (context, index) { - return const YoutubeVideoCardDummy( - shimmerEnabled: true, - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - ); - }, + page == null // we display dummy boxes but shimmer would be disabled + ? SliverToBoxAdapter( + key: Key("${currentId}_feed_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: shimmerEnabled, + child: ListView.builder( + padding: EdgeInsets.zero, + key: Key("${currentId}_feedlist_shimmer"), + physics: const NeverScrollableScrollPhysics(), + itemCount: 15, + shrinkWrap: true, + itemBuilder: (_, __) => dummyVideoCard, + ), ), + ) + : SliverFixedExtentList.builder( + key: Key("${currentId}_feedlist"), + itemExtent: relatedThumbnailItemExtent, + itemCount: page.relatedVideosResult.items.length, + itemBuilder: (context, index) { + final item = page.relatedVideosResult.items[index]; + return switch (item.runtimeType) { + const (StreamInfoItem) => YoutubeVideoCard( + key: Key((item as StreamInfoItem).id), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + isImageImportantInCache: false, + video: item, + playlistID: null, + ), + const (StreamInfoItemShort) => YoutubeShortVideoCard( + key: Key("${(item as StreamInfoItemShort?)?.id}"), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + short: item as StreamInfoItemShort, + playlistID: null, + ), + const (PlaylistInfoItem) => YoutubePlaylistCard( + key: Key((item as PlaylistInfoItem).id), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + playlist: item, + subtitle: item.subtitle, + playOnTap: true, + playingId: currentId, + ), + _ => dummyVideoCard, + }; + }, ), - ) - : SliverFixedExtentList.builder( - key: Key("${currentId}_feedlist"), - itemExtent: relatedThumbnailItemExtent, - itemCount: page.relatedVideosResult.items.length, - itemBuilder: (context, index) { - final item = page.relatedVideosResult.items[index]; - return switch (item.runtimeType) { - const (StreamInfoItem) => YoutubeVideoCard( - key: Key((item as StreamInfoItem).id), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - isImageImportantInCache: false, - video: item, - playlistID: null, - ), - const (StreamInfoItemShort) => YoutubeShortVideoCard( - key: Key("${(item as StreamInfoItemShort?)?.id}"), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - short: item as StreamInfoItemShort, - playlistID: null, - ), - const (PlaylistInfoItem) => YoutubePlaylistCard( - key: Key((item as PlaylistInfoItem).id), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - playlist: item, - subtitle: item.subtitle, - playOnTap: true, - ), - _ => const YoutubeVideoCardDummy( - shimmerEnabled: true, - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - ), - }; - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 12.0)), + const SliverPadding(padding: EdgeInsets.only(top: 12.0)), - // --START-- Comments - ObxO( - rx: settings.ytTopComments, - builder: (ytTopComments) { - if (ytTopComments) return const SliverToBoxAdapter(); - return SliverToBoxAdapter( - key: Key("${currentId}_comments_header"), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: YoutubeCommentsHeader( - displayBackButton: false, + // --START-- Comments + ObxO( + rx: settings.ytTopComments, + builder: (ytTopComments) { + if (ytTopComments) return const SliverToBoxAdapter(); + return SliverToBoxAdapter( + key: Key("${currentId}_comments_header"), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: YoutubeCommentsHeader( + displayBackButton: false, + ), ), - ), - ); - }, - ), - ObxO( - rx: settings.ytTopComments, - builder: (ytTopComments) { - if (ytTopComments) return const SliverToBoxAdapter(); - return ObxO( - rx: YoutubeInfoController.current.isLoadingInitialComments, - builder: (loadingInitial) => loadingInitial - ? SliverToBoxAdapter( - key: Key("${currentId}_comments_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - padding: EdgeInsets.zero, - // key: Key(currentId), - physics: const NeverScrollableScrollPhysics(), - itemCount: 10, - shrinkWrap: true, - itemBuilder: (context, index) { - return const YTCommentCard( - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: null, - videoId: null, - ); - }, + ); + }, + ), + ObxO( + rx: settings.ytTopComments, + builder: (ytTopComments) { + if (ytTopComments) return const SliverToBoxAdapter(); + return ObxO( + rx: YoutubeInfoController.current.isLoadingInitialComments, + builder: (loadingInitial) => loadingInitial + ? SliverToBoxAdapter( + key: Key("${currentId}_comments_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + padding: EdgeInsets.zero, + // key: Key(currentId), + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + shrinkWrap: true, + itemBuilder: (context, index) { + return const YTCommentCard( + margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: null, + videoId: null, + ); + }, + ), ), + ) + : ObxO( + rx: YoutubeInfoController.current.currentComments, + builder: (comments) => comments == null + ? const SliverToBoxAdapter() + : SliverList.builder( + key: Key("${currentId}_comments"), + itemCount: comments.length, + itemBuilder: (context, i) { + final comment = comments[i]; + return YTCommentCard( + key: Key("${comment == null}_${comment?.commentId}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + videoId: currentId, + ); + }, + ), ), - ) - : ObxO( - rx: YoutubeInfoController.current.currentComments, - builder: (comments) => comments == null - ? const SliverToBoxAdapter() - : SliverList.builder( - key: Key("${currentId}_comments"), - itemCount: comments.length, - itemBuilder: (context, i) { - final comment = comments[i]; - return YTCommentCard( - key: Key("${comment == null}_${comment?.commentId}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, - videoId: currentId, - ); - }, - ), - ), - ); - }, - ), - ObxO( - rx: settings.ytTopComments, - builder: (ytTopComments) { - if (ytTopComments) return const SliverToBoxAdapter(); - return ObxO( - rx: YoutubeInfoController.current.isLoadingMoreComments, - builder: (loadingMoreComments) => loadingMoreComments - ? const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(12.0), - child: Center( - child: LoadingIndicator(), + ); + }, + ), + ObxO( + rx: settings.ytTopComments, + builder: (ytTopComments) { + if (ytTopComments) return const SliverToBoxAdapter(); + return ObxO( + rx: YoutubeInfoController.current.isLoadingMoreComments, + builder: (loadingMoreComments) => loadingMoreComments + ? const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Center( + child: LoadingIndicator(), + ), ), - ), - ) - : const SliverToBoxAdapter(), - ); - }, - ), + ) + : const SliverToBoxAdapter(), + ); + }, + ), - const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight)) - ], - ), - ObxO( - rx: _shouldShowGlowUnderVideo, - builder: (shouldShowGlowUnderVideo) { - const containerHeight = 12.0; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: shouldShowGlowUnderVideo - ? Stack( - key: const Key('actual_glow'), - children: [ - Container( - height: containerHeight, - color: mainTheme.scaffoldBackgroundColor, - ), - Container( - height: containerHeight, - transform: Matrix4.translationValues(0, containerHeight / 2, 0), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: mainTheme.scaffoldBackgroundColor, - spreadRadius: containerHeight * 0.25, - offset: const Offset(0, 0), - blurRadius: 8.0, - ), - ], + const SliverPadding(padding: EdgeInsets.only(bottom: kYTQueueSheetMinHeight)) + ], + ), + ObxO( + rx: _shouldShowGlowUnderVideo, + builder: (shouldShowGlowUnderVideo) { + const containerHeight = 12.0; + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: shouldShowGlowUnderVideo + ? Stack( + key: const Key('actual_glow'), + children: [ + Container( + height: containerHeight, + color: mainTheme.scaffoldBackgroundColor, ), - ), - ], - ) - : const SizedBox(key: Key('empty_glow')), - ); - }, - ), - ], + Container( + height: containerHeight, + transform: Matrix4.translationValues(0, containerHeight / 2, 0), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: mainTheme.scaffoldBackgroundColor, + spreadRadius: containerHeight * 0.25, + offset: const Offset(0, 0), + blurRadius: 8.0, + ), + ], + ), + ), + ], + ) + : const SizedBox(key: Key('empty_glow')), + ); + }, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), - ), - rightDragAbsorberWidget, - ytMiniplayerQueueChip, - miniplayerDimWidget, // -- dimming - absorbBottomDragWidget, // prevent accidental scroll while performing home gesture - ], - ); + rightDragAbsorberWidget, + ytMiniplayerQueueChip, + miniplayerDimWidget, // -- dimming + absorbBottomDragWidget, // prevent accidental scroll while performing home gesture + ], + ); - final titleChild = Column( - key: Key("${currentId}_title_button1_child"), - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - borderRadius: 4.0, - height: 16.0, - shimmerEnabled: page == null, - width: maxWidth - 24.0, - child: Text( - videoTitle ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: mainTextTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 13.5, + final titleChild = Column( + key: Key("${currentId}_title_button1_child"), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NamidaDummyContainer( + borderRadius: 4.0, + height: 16.0, + shimmerEnabled: shimmerEnabled, + width: maxWidth - 24.0, + child: Text( + videoTitle ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: mainTextTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 13.5, + ), ), ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - borderRadius: 4.0, - height: 10.0, - shimmerEnabled: page == null, - width: maxWidth - 24.0 * 2, - child: Text( - channelName ?? '', - style: mainTextTheme.displaySmall?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 13.0, + const SizedBox(height: 4.0), + NamidaDummyContainer( + borderRadius: 4.0, + height: 10.0, + shimmerEnabled: shimmerEnabled, + width: maxWidth - 24.0 * 2, + child: Text( + channelName ?? '', + style: mainTextTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 13.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - ], - ); + ], + ); - final playPauseButtonChild = Obx( - () { - final isLoading = Player.inst.shouldShowLoadingIndicatorR; - return Stack( - alignment: Alignment.center, - children: [ - if (isLoading) - IgnorePointer( - child: NamidaOpacity( - key: Key("${currentId}_button_loading"), - enabled: true, - opacity: 0.3, - child: ThreeArchedCircle( - key: Key("${currentId}_button_loading_child"), - color: defaultIconColor, - size: 36.0, + final playPauseButtonChild = Obx( + () { + final isLoading = Player.inst.shouldShowLoadingIndicatorR; + return Stack( + alignment: Alignment.center, + children: [ + if (isLoading) + IgnorePointer( + child: NamidaOpacity( + key: Key("${currentId}_button_loading"), + enabled: true, + opacity: 0.3, + child: ThreeArchedCircle( + key: Key("${currentId}_button_loading_child"), + color: defaultIconColor, + size: 36.0, + ), ), ), + NamidaIconButton( + horizontalPadding: 0.0, + onPressed: () { + Player.inst.togglePlayPause(); + }, + icon: null, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Player.inst.isPlayingR + ? Icon( + Broken.pause, + color: defaultIconColor, + key: const Key('pause'), + ) + : Icon( + Broken.play, + color: defaultIconColor, + key: const Key('play'), + ), + ), ), - NamidaIconButton( - horizontalPadding: 0.0, - onPressed: () { - Player.inst.togglePlayPause(); - }, - icon: null, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Player.inst.isPlayingR - ? Icon( - Broken.pause, - color: defaultIconColor, - key: const Key('pause'), - ) - : Icon( - Broken.play, - color: defaultIconColor, - key: const Key('play'), - ), - ), - ), - ], - ); - }, - ); - final nextButton = NamidaIconButton( - horizontalPadding: 0.0, - icon: Broken.next, - iconColor: defaultIconColor, - onPressed: () { - Player.inst.next(); - }, - ); + ], + ); + }, + ); + final nextButton = NamidaIconButton( + horizontalPadding: 0.0, + icon: Broken.next, + iconColor: defaultIconColor, + onPressed: () { + Player.inst.next(); + }, + ); - return ObxO( - rx: settings.enableBottomNavBar, - builder: (enableBottomNavBar) => ObxO( - rx: settings.dismissibleMiniplayer, - builder: (dismissibleMiniplayer) => NamidaYTMiniplayer( - key: MiniPlayerController.inst.ytMiniplayerKey, - duration: const Duration(milliseconds: 1000), - curve: Curves.easeOutExpo, - bottomMargin: 8.0 + (enableBottomNavBar ? kBottomNavigationBarHeight : 0.0) - 1.0, // -1 is just a clip ensurer. - minHeight: miniplayerHeight, - maxHeight: context.height, - bgColor: miniplayerBGColor, - displayBottomBGLayer: !enableBottomNavBar, - onDismiss: dismissibleMiniplayer ? Player.inst.clearQueue : null, - onDismissing: (dismissPercentage) { - Player.inst.setPlayerVolume(dismissPercentage.clamp(0.0, settings.player.volume.value)); - }, - onHeightChange: (percentage) { - MiniPlayerController.inst.animateMiniplayer(percentage); - }, - onAlternativePercentageExecute: () { - VideoController.inst.toggleFullScreenVideoView( - isLocal: false, - setOrientations: false, - ); - }, - builder: (double height, double p) { - final percentage = (p * 2.8).clamp(0.0, 1.0); - final percentageFast = (p * 1.5 - 0.5).clamp(0.0, 1.0); - final inversePerc = 1 - percentage; - final reverseOpacity = (inversePerc * 2.8 - 1.8).clamp(0.0, 1.0); - final finalspace1sb = space1sb * inversePerc; - final finalspace3sb = space3sb * inversePerc; - final finalspace4buttons = space4 * inversePerc; - final finalspace5sb = space5sb * inversePerc; - final finalpadding = 4.0 * inversePerc; - final finalbr = (8.0 * inversePerc).multipliedRadius; - final finalthumbnailWidth = (space2ForThumbnail + maxWidth * percentage).clamp(space2ForThumbnail, maxWidth - finalspace1sb - finalspace3sb); - final finalthumbnailHeight = finalthumbnailWidth * 9 / 16; + return ObxO( + rx: settings.enableBottomNavBar, + builder: (enableBottomNavBar) => ObxO( + rx: settings.dismissibleMiniplayer, + builder: (dismissibleMiniplayer) => NamidaYTMiniplayer( + key: MiniPlayerController.inst.ytMiniplayerKey, + duration: const Duration(milliseconds: 1000), + curve: Curves.easeOutExpo, + bottomMargin: 8.0 + (enableBottomNavBar ? kBottomNavigationBarHeight : 0.0) - 1.0, // -1 is just a clip ensurer. + minHeight: miniplayerHeight, + maxHeight: context.height, + bgColor: miniplayerBGColor, + displayBottomBGLayer: !enableBottomNavBar, + onDismiss: dismissibleMiniplayer ? Player.inst.clearQueue : null, + onDismissing: (dismissPercentage) { + Player.inst.setPlayerVolume(dismissPercentage.clamp(0.0, settings.player.volume.value)); + }, + onHeightChange: (percentage) { + MiniPlayerController.inst.animateMiniplayer(percentage); + }, + onAlternativePercentageExecute: () { + VideoController.inst.toggleFullScreenVideoView( + isLocal: false, + setOrientations: false, + ); + }, + builder: (double height, double p) { + final percentage = (p * 2.8).clamp(0.0, 1.0); + final percentageFast = (p * 1.5 - 0.5).clamp(0.0, 1.0); + final inversePerc = 1 - percentage; + final reverseOpacity = (inversePerc * 2.8 - 1.8).clamp(0.0, 1.0); + final finalspace1sb = space1sb * inversePerc; + final finalspace3sb = space3sb * inversePerc; + final finalspace4buttons = space4 * inversePerc; + final finalspace5sb = space5sb * inversePerc; + final finalpadding = 4.0 * inversePerc; + final finalbr = (8.0 * inversePerc).multipliedRadius; + final finalthumbnailWidth = (space2ForThumbnail + maxWidth * percentage).clamp(space2ForThumbnail, maxWidth - finalspace1sb - finalspace3sb); + final finalthumbnailHeight = finalthumbnailWidth * 9 / 16; - return Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox(width: finalspace1sb), - Container( - clipBehavior: Clip.antiAlias, - margin: EdgeInsets.symmetric(vertical: finalpadding), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(finalbr), - ), - width: finalthumbnailWidth, - height: finalthumbnailHeight, - child: NamidaVideoWidget( - isLocal: false, - enableControls: percentage > 0.5, - onMinimizeTap: () => MiniPlayerController.inst.ytMiniplayerKey.currentState?.animateToState(false), - swipeUpToFullscreen: true, + return Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox(width: finalspace1sb), + Container( + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.symmetric(vertical: finalpadding), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(finalbr), + ), + width: finalthumbnailWidth, + height: finalthumbnailHeight, + child: NamidaVideoWidget( + isLocal: false, + enableControls: percentage > 0.5, + onMinimizeTap: () => MiniPlayerController.inst.ytMiniplayerKey.currentState?.animateToState(false), + swipeUpToFullscreen: true, + ), ), - ), - if (reverseOpacity > 0) ...[ - SizedBox(width: finalspace3sb), - SizedBox( - width: (maxWidth - finalthumbnailWidth - finalspace1sb - finalspace3sb - finalspace4buttons - finalspace5sb).clamp(0, maxWidth), - child: NamidaOpacity( - key: Key("${currentId}_title_button1"), + if (reverseOpacity > 0) ...[ + SizedBox(width: finalspace3sb), + SizedBox( + width: (maxWidth - finalthumbnailWidth - finalspace1sb - finalspace3sb - finalspace4buttons - finalspace5sb).clamp(0, maxWidth), + child: NamidaOpacity( + key: Key("${currentId}_title_button1"), + enabled: true, + opacity: reverseOpacity, + child: titleChild, + ), + ), + NamidaOpacity( + key: Key("${currentId}_title_button2"), enabled: true, opacity: reverseOpacity, - child: titleChild, - ), - ), - NamidaOpacity( - key: Key("${currentId}_title_button2"), - enabled: true, - opacity: reverseOpacity, - child: SizedBox( - key: Key("${currentId}_title_button2_child"), - width: finalspace4buttons / 2, - height: miniplayerHeight, - child: playPauseButtonChild, + child: SizedBox( + key: Key("${currentId}_title_button2_child"), + width: finalspace4buttons / 2, + height: miniplayerHeight, + child: playPauseButtonChild, + ), ), - ), - NamidaOpacity( - key: Key("${currentId}_title_button3"), - enabled: true, - opacity: reverseOpacity, - child: SizedBox( - key: Key("${currentId}_title_button3_child"), - width: finalspace4buttons / 2, - height: miniplayerHeight, - child: nextButton, + NamidaOpacity( + key: Key("${currentId}_title_button3"), + enabled: true, + opacity: reverseOpacity, + child: SizedBox( + key: Key("${currentId}_title_button3_child"), + width: finalspace4buttons / 2, + height: miniplayerHeight, + child: nextButton, + ), ), - ), - SizedBox(width: finalspace5sb), - ] - ], - ), + SizedBox(width: finalspace5sb), + ] + ], + ), - // ---- if was in comments subpage, and this gets hidden, the route is popped - // ---- same with [isQueueSheetOpen] - if (NamidaNavigator.inst.isInYTCommentsSubpage || NamidaNavigator.inst.isQueueSheetOpen ? true : percentage > 0) - Expanded( - child: Stack( - fit: StackFit.expand, - children: [ - miniplayerBody, - IgnorePointer( - child: ColoredBox( - color: miniplayerBGColor.withOpacity(1 - percentageFast), + // ---- if was in comments subpage, and this gets hidden, the route is popped + // ---- same with [isQueueSheetOpen] + if (NamidaNavigator.inst.isInYTCommentsSubpage || NamidaNavigator.inst.isQueueSheetOpen ? true : percentage > 0) + Expanded( + child: Stack( + fit: StackFit.expand, + children: [ + miniplayerBody, + IgnorePointer( + child: ColoredBox( + color: miniplayerBGColor.withOpacity(1 - percentageFast), + ), ), - ), - ], + ], + ), ), - ), - ], - ), - Positioned( - top: finalthumbnailHeight - - (_extraPaddingForYTMiniplayer / 2 * (1 - percentage)) - - (SeekReadyDimensions.barHeight / 2) - - (SeekReadyDimensions.barHeight / 2 * percentage) + - (SeekReadyDimensions.progressBarHeight / 2), - left: 0, - right: 0, - child: seekReadyWidget, - ), - ], - ); - }, + ], + ), + Positioned( + top: finalthumbnailHeight - + (_extraPaddingForYTMiniplayer / 2 * (1 - percentage)) - + (SeekReadyDimensions.barHeight / 2) - + (SeekReadyDimensions.barHeight / 2 * percentage) + + (SeekReadyDimensions.progressBarHeight / 2), + left: 0, + right: 0, + child: seekReadyWidget, + ), + ], + ); + }, + ), ), - ), - ); - }, + ); + }, + ), ), ); }, diff --git a/lib/youtube/youtube_playlists_view.dart b/lib/youtube/youtube_playlists_view.dart index 49fe3c39..d35d2d4d 100644 --- a/lib/youtube/youtube_playlists_view.dart +++ b/lib/youtube/youtube_playlists_view.dart @@ -25,6 +25,7 @@ import 'package:namida/youtube/pages/yt_history_page.dart'; import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; import 'package:namida/youtube/widgets/yt_card.dart'; import 'package:namida/youtube/widgets/yt_history_video_card.dart'; +import 'package:namida/youtube/widgets/yt_thumbnail.dart'; import 'package:namida/youtube/yt_utils.dart'; class YoutubePlaylistsView extends StatelessWidget with NamidaRouteWidget { @@ -269,6 +270,7 @@ class YoutubePlaylistsView extends StatelessWidget with NamidaRouteWidget { childrenDefault: displayMenu ? () => getMenuItems(playlist) : null, openOnTap: false, child: YoutubeCard( + thumbnailType: ThumbnailType.playlist, isImageImportantInCache: true, extractColor: true, thumbnailWidthPercentage: 0.75, diff --git a/pubspec.yaml b/pubspec.yaml index b1717e5d..1d36bf7b 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.9-beta+240702128 +version: 2.9.0-beta+240702229 environment: sdk: ">=3.4.0 <4.0.0"