From 8bf29acd5bf594f29bc28673eb9e26250e89d6d0 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Tue, 9 Jul 2024 01:52:11 +0300 Subject: [PATCH] chore(yt): fixes etc fixes etc --- lib/base/pull_to_refresh.dart | 7 +- lib/controller/logs_controller.dart | 4 +- lib/ui/widgets/video_widget.dart | 4 + .../controller/youtube_current_info.dart | 1 + .../controller/youtube_info_controller.dart | 30 ++- lib/youtube/functions/download_sheet.dart | 247 +++++++++--------- .../functions/video_download_options.dart | 18 +- .../youtube_manage_subscription_page.dart | 6 +- .../youtube_main_page_fetcher_acc_base.dart | 4 +- lib/youtube/pages/yt_channel_subpage.dart | 7 +- lib/youtube/pages/yt_channels_page.dart | 4 +- lib/youtube/pages/yt_playlist_subpage.dart | 16 +- pubspec.yaml | 2 +- 13 files changed, 203 insertions(+), 147 deletions(-) diff --git a/lib/base/pull_to_refresh.dart b/lib/base/pull_to_refresh.dart index ff0b39c8..17c60c58 100644 --- a/lib/base/pull_to_refresh.dart +++ b/lib/base/pull_to_refresh.dart @@ -92,12 +92,15 @@ mixin PullToRefreshMixin on State implements Ticker } bool _isRefreshing = false; - Future onRefresh(PullToRefreshCallback execute, {bool forceShow = false}) async { + + /// Normally will not continue if not fully swiped, [forceProceed] forces bypassing that, + /// use if action not triggered by user but u want to show refresh indicator. + Future onRefresh(PullToRefreshCallback execute, {bool forceProceed = false}) async { if (!enablePullToRefresh) return; onVerticalDragFinish(); if (_isRefreshing) return; if (animation.value != 1) { - if (!forceShow) return; + if (!forceProceed) return; animation.animateTo(1, duration: const Duration(milliseconds: 50)); } diff --git a/lib/controller/logs_controller.dart b/lib/controller/logs_controller.dart index 9aa2cc82..5b5af71a 100644 --- a/lib/controller/logs_controller.dart +++ b/lib/controller/logs_controller.dart @@ -22,8 +22,8 @@ class _Log { printer: PrettyPrinter( colors: kDebugMode ? true : false, printEmojis: true, - methodCount: 4, - errorMethodCount: 24, + methodCount: 48, + errorMethodCount: 48, ), output: _FileOutput(file: File(AppPaths.LOGS), fileClean: File(AppPaths.LOGS_CLEAN)), ); diff --git a/lib/ui/widgets/video_widget.dart b/lib/ui/widgets/video_widget.dart index b83eafe0..70623e05 100644 --- a/lib/ui/widgets/video_widget.dart +++ b/lib/ui/widgets/video_widget.dart @@ -1177,6 +1177,10 @@ class NamidaVideoControlsState extends State with TickerPro qt = video == null ? null : '${video.resolution}p${video.framerateText()}'; } else { qt = Player.inst.currentVideoStream.valueR?.qualityLabel; + if (qt == null) { + final cached = Player.inst.currentCachedVideo.valueR; + if (cached != null) qt = "${cached.resolution}p${cached.framerateText()}"; + } } } diff --git a/lib/youtube/controller/youtube_current_info.dart b/lib/youtube/controller/youtube_current_info.dart index ac3b5b12..d8e679d1 100644 --- a/lib/youtube/controller/youtube_current_info.dart +++ b/lib/youtube/controller/youtube_current_info.dart @@ -139,6 +139,7 @@ class _YoutubeCurrentInfoController { details: ExecuteDetails.forceRequest(), ); if (newRes != null && _canSafelyModifyMetadata(videoId)) { + fetchedSuccessfully = true; _currentComments.value = newRes; _isCurrentCommentsFromCache.value = false; } diff --git a/lib/youtube/controller/youtube_info_controller.dart b/lib/youtube/controller/youtube_info_controller.dart index a85e45c6..6250bc27 100644 --- a/lib/youtube/controller/youtube_info_controller.dart +++ b/lib/youtube/controller/youtube_info_controller.dart @@ -2,9 +2,9 @@ library namidayoutubeinfo; import 'dart:io'; +import 'package:logger/logger.dart'; import 'package:namida/class/video.dart'; import 'package:namida/controller/connectivity.dart'; -import 'package:namida/controller/logs_controller.dart' as namidalogs; import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/player_controller.dart'; import 'package:namida/controller/settings_controller.dart'; @@ -54,7 +54,7 @@ class YoutubeInfoController { sensitiveDataDirectory: AppDirs.YOUTIPIE_DATA, checkJSPlayer: true, // wont await.. are we cooked? properly ); - YoutiPie.setLogs(namidalogs.logger.logger); + YoutiPie.setLogs(_YTReportingLog()); } static Future ensureJSPlayerInitialized() async { @@ -65,3 +65,29 @@ class YoutubeInfoController { static final current = _YoutubeCurrentInfoController._(); static final utils = _YoutubeInfoUtils._(); } + +class _YTReportingLog extends Logger { + static void _showError(String msg, {Object? exception}) { + String title = lang.ERROR; + if (exception != null) title += ': $exception'; + + snackyy( + message: msg, + title: title, + isError: true, + displaySeconds: 3, + top: false, + ); + } + + @override + void e( + dynamic message, { + DateTime? time, + Object? error, + StackTrace? stackTrace, + }) { + _showError(message.toString(), exception: error); + super.e(message, time: time, error: error, stackTrace: stackTrace); + } +} diff --git a/lib/youtube/functions/download_sheet.dart b/lib/youtube/functions/download_sheet.dart index 8567186e..91346780 100644 --- a/lib/youtube/functions/download_sheet.dart +++ b/lib/youtube/functions/download_sheet.dart @@ -242,6 +242,7 @@ Future showDownloadVideoBottomSheet({ final IconData? icon, final Widget? leading, final TextStyle? style, + required final bool hasWebm, required final void Function()? onSussyIconTap, required final void Function() onCloseIconTap, }) { @@ -265,14 +266,15 @@ Future showDownloadVideoBottomSheet({ ), ], const Spacer(), - NamidaIconButton( - tooltip: () => lang.SHOW_WEBM, - horizontalPadding: 0.0, - iconSize: 20.0, - icon: Broken.video_octagon, - onPressed: onSussyIconTap, - ), - const SizedBox(width: 12.0), + if (hasWebm) + NamidaIconButton( + tooltip: () => lang.SHOW_WEBM, + horizontalPadding: 0.0, + iconSize: 20.0, + icon: Broken.video_octagon, + onPressed: onSussyIconTap, + ), + if (hasWebm) const SizedBox(width: 12.0), NamidaIconButton( horizontalPadding: 0.0, iconSize: 20.0, @@ -430,125 +432,132 @@ Future showDownloadVideoBottomSheet({ ), ), Expanded( - child: ListView( - shrinkWrap: true, - children: [ - Obx( - () { - final e = selectedAudioOnlyStream.valueR; - final subtitle = e == null ? null : "${e.bitrateText()} • ${e.codecInfo.container} • ${e.sizeInBytes.fileSizeFormatted}"; - return getTextWidget( - title: lang.AUDIO, - subtitle: subtitle, - icon: Broken.audio_square, - onCloseIconTap: () { - selectedAudioOnlyStream.value = null; - onAudioSelectionChanged(); - }, - onSussyIconTap: () { - showAudioWebm.toggle(); - if (showAudioWebm.value == false && selectedAudioOnlyStream.value?.isWebm == true) { - selectedAudioOnlyStream.value = streamResult.value?.audioStreams.firstOrNull; - onAudioSelectionChanged(); - } + child: ObxO( + rx: streamResult, + builder: (streamResult) { + final hasAudioWebm = streamResult?.audioStreams.firstWhereEff((e) => e.isWebm) != null; + final hasVideoWebm = streamResult?.videoStreams.firstWhereEff((e) => e.isWebm) != null; + return ListView( + shrinkWrap: true, + children: [ + Obx( + () { + final e = selectedAudioOnlyStream.valueR; + final subtitle = e == null ? null : "${e.bitrateText()} • ${e.codecInfo.container} • ${e.sizeInBytes.fileSizeFormatted}"; + return getTextWidget( + hasWebm: hasAudioWebm, + title: lang.AUDIO, + subtitle: subtitle, + icon: Broken.audio_square, + onCloseIconTap: () { + selectedAudioOnlyStream.value = null; + onAudioSelectionChanged(); + }, + onSussyIconTap: () { + showAudioWebm.toggle(); + if (showAudioWebm.value == false && selectedAudioOnlyStream.value?.isWebm == true) { + selectedAudioOnlyStream.value = streamResult?.audioStreams.firstOrNull; + onAudioSelectionChanged(); + } + }, + ); }, - ); - }, - ), - ObxO( - rx: streamResult, - builder: (streamResult) => streamResult?.audioStreams == null - ? Padding( - padding: const EdgeInsets.all(8.0), - child: ShimmerWrapper( - shimmerEnabled: true, - child: getPopupItem( - items: List.filled(2, null), - itemBuilder: (item) => getDummyQualityChip(width: 164.0), + ), + streamResult?.audioStreams == null + ? Padding( + padding: const EdgeInsets.all(8.0), + child: ShimmerWrapper( + shimmerEnabled: true, + child: getPopupItem( + items: List.filled(2, null), + itemBuilder: (item) => getDummyQualityChip(width: 164.0), + ), + ), + ) + : ObxO( + rx: showAudioWebm, + builder: (showAudioWebm) => getPopupItem( + items: showAudioWebm ? streamResult!.audioStreams : streamResult!.audioStreams.where((element) => !element.isWebm).toList(), + itemBuilder: (element) { + return Obx( + () { + final cacheFile = element.getCachedFile(videoId); + return getQualityButton( + selected: selectedAudioOnlyStream.valueR == element, + cacheExists: cacheFile != null, + title: "${element.codecInfo.codec} • ${element.sizeInBytes.fileSizeFormatted}", + subtitle: "${element.codecInfo.container} • ${element.bitrateText()}", + onTap: () { + selectedAudioOnlyStream.value = element; + onAudioSelectionChanged(); + }, + ); + }, + ); + }, ), ), - ) - : ObxO( - rx: showAudioWebm, - builder: (showAudioWebm) => getPopupItem( - items: showAudioWebm ? streamResult!.audioStreams : streamResult!.audioStreams.where((element) => !element.isWebm).toList(), - itemBuilder: (element) { - return Obx( - () { - final cacheFile = element.getCachedFile(videoId); - return getQualityButton( - selected: selectedAudioOnlyStream.valueR == element, - cacheExists: cacheFile != null, - title: "${element.codecInfo.codec} • ${element.sizeInBytes.fileSizeFormatted}", - subtitle: "${element.codecInfo.container} • ${element.bitrateText()}", - onTap: () { - selectedAudioOnlyStream.value = element; - onAudioSelectionChanged(); - }, - ); - }, - ); - }, - ), - ), - ), - getDivider(), - ObxO( - rx: selectedVideoOnlyStream, - builder: (vostream) { - final subtitle = vostream == null ? null : "${vostream.qualityLabel} • ${vostream.sizeInBytes.fileSizeFormatted}"; - return getTextWidget( - title: lang.VIDEO, - subtitle: subtitle, - icon: Broken.video_square, - onCloseIconTap: () { - selectedVideoOnlyStream.value = null; - onVideoSelectionChanged(); - }, - onSussyIconTap: () { - showVideoWebm.toggle(); - if (showVideoWebm.value == false && selectedVideoOnlyStream.value?.isWebm == true) { - selectedVideoOnlyStream.value = streamResult.value?.videoStreams.firstOrNull; - onVideoSelectionChanged(); - } + getDivider(), + ObxO( + rx: selectedVideoOnlyStream, + builder: (vostream) { + final subtitle = vostream == null ? null : "${vostream.qualityLabel} • ${vostream.sizeInBytes.fileSizeFormatted}"; + return getTextWidget( + hasWebm: hasVideoWebm, + title: lang.VIDEO, + subtitle: subtitle, + icon: Broken.video_square, + onCloseIconTap: () { + selectedVideoOnlyStream.value = null; + onVideoSelectionChanged(); + }, + onSussyIconTap: () { + showVideoWebm.toggle(); + if (showVideoWebm.value == false && selectedVideoOnlyStream.value?.isWebm == true) { + selectedVideoOnlyStream.value = streamResult?.videoStreams.firstOrNull; + onVideoSelectionChanged(); + } + }, + ); }, - ); - }, - ), - Obx( - () => streamResult.valueR?.videoStreams == null - ? Padding( - padding: const EdgeInsets.all(8.0), - child: ShimmerWrapper( - shimmerEnabled: true, - child: getPopupItem( - items: List.filled(8, null), - itemBuilder: (item) => getDummyQualityChip(), + ), + streamResult?.videoStreams == null + ? Padding( + padding: const EdgeInsets.all(8.0), + child: ShimmerWrapper( + shimmerEnabled: true, + child: getPopupItem( + items: List.filled(8, null), + itemBuilder: (item) => getDummyQualityChip(), + ), ), - ), - ) - : getPopupItem( - items: showVideoWebm.valueR ? streamResult.valueR!.videoStreams : streamResult.valueR!.videoStreams.where((element) => !element.isWebm).toList(), - itemBuilder: (element) { - return Obx( - () { - final cacheFile = element.getCachedFile(videoId); - return getQualityButton( - selected: selectedVideoOnlyStream.valueR == element, - cacheExists: cacheFile != null, - title: "${element.qualityLabel} • ${element.sizeInBytes.fileSizeFormatted}", - subtitle: "${element.codecInfo.container} • ${element.bitrateText()}", - onTap: () { - selectedVideoOnlyStream.value = element; - onVideoSelectionChanged(); + ) + : ObxO( + rx: showVideoWebm, + builder: (showVideoWebm) => getPopupItem( + items: showVideoWebm ? streamResult!.videoStreams : streamResult!.videoStreams.where((element) => !element.isWebm).toList(), + itemBuilder: (element) { + return Obx( + () { + final cacheFile = element.getCachedFile(videoId); + return getQualityButton( + selected: selectedVideoOnlyStream.valueR == element, + cacheExists: cacheFile != null, + title: "${element.qualityLabel} • ${element.sizeInBytes.fileSizeFormatted}", + subtitle: "${element.codecInfo.container} • ${element.bitrateText()}", + onTap: () { + selectedVideoOnlyStream.value = element; + onVideoSelectionChanged(); + }, + ); }, ); }, - ); - }, - ), - ), - ], + ), + ), + ], + ); + }, ), ), const SizedBox(height: 12.0), diff --git a/lib/youtube/functions/video_download_options.dart b/lib/youtube/functions/video_download_options.dart index 970346dd..77d4f9f1 100644 --- a/lib/youtube/functions/video_download_options.dart +++ b/lib/youtube/functions/video_download_options.dart @@ -344,7 +344,7 @@ class YTDownloadOptionFolderListTileState extends State [ NamidaPopupItem( icon: Broken.add, @@ -376,7 +376,7 @@ class YTDownloadOptionFolderListTileState extends State _onPatreonLoginTap(context), + onTap: () => _onPatreonLoginTap(context, signInDecision: SignInDecision.forceSignIn), ), ], ), diff --git a/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart b/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart index 9e2e90f3..c60f84a7 100644 --- a/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart +++ b/lib/youtube/pages/youtube_main_page_fetcher_acc_base.dart @@ -94,8 +94,10 @@ class _YoutubePageState, T extends MapSerializa } Future _fetchFeedNext() async { - _isLoadingNext.value = true; final feed = _currentFeed; + if (feed.value?.canFetchNext != true) return; + + _isLoadingNext.value = true; final fetched = await feed.value?.fetchNext(); if (fetched == true) feed.refresh(); _isLoadingNext.value = false; diff --git a/lib/youtube/pages/yt_channel_subpage.dart b/lib/youtube/pages/yt_channel_subpage.dart index efda3025..15408816 100644 --- a/lib/youtube/pages/yt_channel_subpage.dart +++ b/lib/youtube/pages/yt_channel_subpage.dart @@ -77,7 +77,7 @@ class _YTChannelSubpageState extends YoutubeChannelController (value) { if (value != null) { setState(() => _channelInfo = value); - onRefresh(() => fetchChannelStreams(value, forceRequest: true), forceShow: true); + onRefresh(() => fetchChannelStreams(value, forceRequest: true), forceProceed: true); } }, ); @@ -249,6 +249,7 @@ class _YTChannelSubpageState extends YoutubeChannelController videosCountVSTotalText += ' | '; peakDatesText = "${streamsPeakDates!.oldest.millisecondsSinceEpoch.dateFormattedOriginal} (${Jiffy.parseFromDateTime(streamsPeakDates!.oldest).fromNow()})"; } + final hasMoreStreamsLeft = channelVideoTab?.canFetchNext == true; return BackgroundWrapper( child: Listener( onPointerMove: (event) => onPointerMove(uploadsScrollController, event), @@ -364,9 +365,9 @@ class _YTChannelSubpageState extends YoutubeChannelController borderRadius: 8.0, icon: Broken.task_square, text: lang.LOAD_ALL, - enabled: !isLoadingMoreUploads, + enabled: !isLoadingMoreUploads && hasMoreStreamsLeft, disableWhenLoading: false, - showLoadingWhenDisabled: true, + showLoadingWhenDisabled: hasMoreStreamsLeft, onTap: _onLoadAllTap, ), ), diff --git a/lib/youtube/pages/yt_channels_page.dart b/lib/youtube/pages/yt_channels_page.dart index bb367efa..c88c8d37 100644 --- a/lib/youtube/pages/yt_channels_page.dart +++ b/lib/youtube/pages/yt_channels_page.dart @@ -58,7 +58,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController _updateChannel(sub, forceRequest: true), forceShow: true); + onRefresh(() => _updateChannel(sub, forceRequest: true), forceProceed: true); } final now = DateTime.now(); @@ -373,7 +373,7 @@ class _YoutubeChannelsPageState extends YoutubeChannelController onPointerMove(uploadsScrollController, event), - onPointerUp: (_) => channel == null ? null : onRefresh(() => _updateChannel(channel!, forceRequest: true), forceShow: true), + onPointerUp: (_) => channel == null ? null : onRefresh(() => _updateChannel(channel!, forceRequest: true)), onPointerCancel: (_) => onVerticalDragFinish(), child: isLoadingInitialStreams ? ShimmerWrapper( diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart index ddbb4fb6..c0b2e1d5 100644 --- a/lib/youtube/pages/yt_playlist_subpage.dart +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -438,21 +438,27 @@ class _YTHostedPlaylistSubpageState extends State with if (_isLoadingMoreItems.value) return; _isLoadingMoreItems.value = true; + bool fetched = false; + try { if (_playlist.items.isEmpty) { final playlist = await YoutubeInfoController.playlist.fetchPlaylist( playlistId: _playlist.basicInfo.id, details: ExecuteDetails.forceRequest(), ); - if (playlist != null) _playlist = playlist; + if (playlist != null) { + _playlist = playlist; + fetched = true; + } } else { - await _playlist.fetchNext(); + if (_playlist.canFetchNext) { + fetched = await _playlist.fetchNext(); + } } } catch (_) {} - trySortStreams(); _isLoadingMoreItems.value = false; - refreshState(); + if (fetched) refreshState(trySortStreams); } @override @@ -650,7 +656,7 @@ class _YTHostedPlaylistSubpageState extends State with text: lang.LOAD_ALL, enabled: !isLoadingMoreItems && hasMoreStreamsLeft, // this for lazylist disableWhenLoading: false, - showLoadingWhenDisabled: true, + showLoadingWhenDisabled: hasMoreStreamsLeft, onTap: () async { if (_currentFetchAllRes != null) { _currentFetchAllRes?.cancel(); diff --git a/pubspec.yaml b/pubspec.yaml index 818aa968..a0e7ec55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 3.0.6-beta+240707211 +version: 3.0.7-beta+240708228 environment: sdk: ">=3.4.0 <4.0.0"