diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index 9c947031..a23d1b63 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -204,95 +204,637 @@ class _NamidaMiniPlayerState extends State { @override Widget build(BuildContext context) { + final onSecondary = context.theme.colorScheme.onSecondaryContainer; return MiniplayerRaw( - constantChild: SafeArea( - bottom: false, - child: Padding( - padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 70), - child: ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32.0.multipliedRadius), - topRight: Radius.circular(32.0.multipliedRadius), - ), - child: Column( - children: [ - Expanded( - child: Obx( - () { - final currentIndex = Player.inst.currentIndex; - return NamidaListView( - key: const Key('minikuru'), - itemExtents: List.filled(Player.inst.currentQueue.length, Dimensions.inst.trackTileItemExtent), - scrollController: MiniPlayerController.inst.queueScrollController, - padding: EdgeInsets.only(bottom: 8.0 + SelectedTracksController.inst.bottomPadding.value), - onReorderStart: (index) => MiniPlayerController.inst.invokeStartReordering(), - onReorderEnd: (index) => MiniPlayerController.inst.invokeDoneReordering(), - onReorder: (oldIndex, newIndex) => Player.inst.reorderTrack(oldIndex, newIndex), - itemCount: Player.inst.currentQueue.length, - itemBuilder: (context, i) { - final track = Player.inst.currentQueue[i]; - final key = "$i${track.track.path}"; - return FadeDismissible( - key: Key("Diss_$key"), - onDismissed: (direction) { - Player.inst.removeFromQueue(i); - MiniPlayerController.inst.invokeDoneReordering(); - }, - onUpdate: (details) { - final isReordering = details.progress != 0.0; - if (isReordering) { - MiniPlayerController.inst.invokeStartReordering(); - } else { + constantChildren: [ + // constant [0] -- queue + SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 70), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32.0.multipliedRadius), + topRight: Radius.circular(32.0.multipliedRadius), + ), + child: Column( + children: [ + Expanded( + child: Obx( + () { + final currentIndex = Player.inst.currentIndex; + return NamidaListView( + key: const Key('minikuru'), + itemExtents: List.filled(Player.inst.currentQueue.length, Dimensions.inst.trackTileItemExtent), + scrollController: MiniPlayerController.inst.queueScrollController, + padding: EdgeInsets.only(bottom: 8.0 + SelectedTracksController.inst.bottomPadding.value), + onReorderStart: (index) => MiniPlayerController.inst.invokeStartReordering(), + onReorderEnd: (index) => MiniPlayerController.inst.invokeDoneReordering(), + onReorder: (oldIndex, newIndex) => Player.inst.reorderTrack(oldIndex, newIndex), + itemCount: Player.inst.currentQueue.length, + itemBuilder: (context, i) { + final track = Player.inst.currentQueue[i]; + final key = "$i${track.track.path}"; + return FadeDismissible( + key: Key("Diss_$key"), + onDismissed: (direction) { + Player.inst.removeFromQueue(i); MiniPlayerController.inst.invokeDoneReordering(); - } - }, - child: TrackTile( - key: Key("tt_$key"), - index: i, - trackOrTwd: track, - displayRightDragHandler: true, - draggableThumbnail: true, - queueSource: QueueSource.playerQueue, - cardColorOpacity: 0.5, - fadeOpacity: i < currentIndex ? 0.3 : 0.0, - onPlaying: () { - // -- to improve performance, skipping process of checking new queues, etc.. - if (i == currentIndex) { - Player.inst.togglePlayPause(); + }, + onUpdate: (details) { + final isReordering = details.progress != 0.0; + if (isReordering) { + MiniPlayerController.inst.invokeStartReordering(); } else { - Player.inst.skipToQueueItem(i); + MiniPlayerController.inst.invokeDoneReordering(); } }, - ), - ); - }, - ); - }, + child: TrackTile( + key: Key("tt_$key"), + index: i, + trackOrTwd: track, + displayRightDragHandler: true, + draggableThumbnail: true, + queueSource: QueueSource.playerQueue, + cardColorOpacity: 0.5, + fadeOpacity: i < currentIndex ? 0.3 : 0.0, + onPlaying: () { + // -- to improve performance, skipping process of checking new queues, etc.. + if (i == currentIndex) { + Player.inst.togglePlayPause(); + } else { + Player.inst.skipToQueueItem(i); + } + }, + ), + ); + }, + ); + }, + ), + ), + Container( + width: context.width, + height: kQueueBottomRowHeight, + decoration: BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.0.multipliedRadius), + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: FittedBox( + child: _queueUtilsRow(context), + ), + ), ), + ], + ), + ), + ), + ), + // constant [1] -- top row + Obx( + () { + final currentIndex = Player.inst.currentIndex; + final currentTrack = Player.inst.nowPlayingTrack; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: MiniPlayerController.inst.snapToMini, + icon: Icon(Broken.arrow_down_2, color: onSecondary), + iconSize: 22.0, ), - Container( - width: context.width, - height: kQueueBottomRowHeight, - decoration: BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.0.multipliedRadius), + Expanded( + child: NamidaInkWell( + borderRadius: 14.0, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + onTap: () => NamidaOnTaps.inst.onAlbumTap(currentTrack.albumIdentifier), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${currentIndex + 1}/${Player.inst.currentQueue.length}", + style: TextStyle( + color: onSecondary.withOpacity(.8), + fontSize: 12.0.multipliedFontScale, + fontWeight: FontWeight.w500, + ), + ), + Text( + currentTrack.album, + textAlign: TextAlign.center, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0.multipliedFontScale, color: onSecondary.withOpacity(.9)), + ), + ], ), ), - child: Padding( + ), + IconButton( + onPressed: () { + NamidaDialogs.inst.showTrackDialog(currentTrack, source: QueueSource.playerQueue); + }, + icon: Container( padding: const EdgeInsets.all(4.0), - child: FittedBox( - child: _queueUtilsRow(context), + decoration: BoxDecoration( + color: context.theme.colorScheme.secondary.withOpacity(.2), + shape: BoxShape.circle, ), + child: Icon(Broken.more, color: onSecondary), ), + iconSize: 22.0, ), ], + ); + }, + ), + + // constant [2] -- pos & dur + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () => Player.inst.seekSecondsBackward(), + onLongPress: () => Player.inst.seek(Duration.zero), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () { + final seek = MiniPlayerController.inst.seekValue.value; + final diffInMs = seek - Player.inst.nowPlayingPosition; + final plusOrMinus = diffInMs < 0 ? '-' : '+'; + final seekText = seek == 0 ? '00:00' : diffInMs.abs().milliSecondsLabel; + return Text( + "$plusOrMinus$seekText", + style: context.textTheme.displaySmall?.copyWith(fontSize: 10.0.multipliedFontScale), + ).animateEntrance( + showWhen: seek != 0, + durationMS: 700, + allCurves: Curves.easeInOutQuart, + ); + }, + ), + NamidaHero( + tag: 'MINIPLAYER_POSITION', + child: Obx( + () => Text( + Player.inst.nowPlayingPosition.milliSecondsLabel, + style: context.textTheme.displaySmall, + ), + ), + ), + ], + ), + ), + ), + GestureDetector( + onTap: () => Player.inst.seekSecondsForward(), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Obx( + () { + final currentDurationInMS = Player.inst.nowPlayingTrack.duration * 1000; + return NamidaHero( + tag: 'MINIPLAYER_DURATION', + child: Obx( + () { + final displayRemaining = settings.displayRemainingDurInsteadOfTotal.value; + final toSubtract = displayRemaining ? Player.inst.nowPlayingPosition : 0; + final msToDisplay = currentDurationInMS - toSubtract; + final prefix = displayRemaining ? '-' : ''; + return Text( + "$prefix ${msToDisplay.milliSecondsLabel}", + style: context.textTheme.displaySmall, + ); + }, + ), + ); + }, + ), + ), + ), + ], + ), + + // constant [3] -- bottom left button + Align( + alignment: Alignment.bottomLeft, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + child: FocusedMenuHolder( + menuOpenAlignment: Alignment.bottomLeft, + bottomOffsetHeight: 12.0, + leftOffsetHeight: 4.0, + onMenuOpen: () { + if (settings.enableVideoPlayback.value) { + isMenuOpened.value = true; + return true; + } else { + ScrollSearchController.inst.unfocusKeyboard(); + NamidaNavigator.inst.navigateDialog(dialog: const Dialog(child: PlaybackSettings(isInDialog: true))); + return false; + } + }, + onMenuClose: () => isMenuOpened.value = false, + blurSize: 2.0, + duration: animationDuration, + animateMenuItems: false, + menuWidth: context.width * 0.5, + menuBoxDecoration: BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12.0.multipliedRadius), + ), + menuWidget: Obx( + () { + final currentTrack = Player.inst.nowPlayingTrack; + final availableVideos = VideoController.inst.currentPossibleVideos; + final ytVideos = VideoController.inst.currentYTQualities.where((s) => s.formatSuffix != 'webm'); + return ListView( + padding: const EdgeInsets.symmetric(vertical: 12.0), + children: [ + _MPQualityButton( + title: lang.CHECK_FOR_MORE, + icon: Broken.chart, + bgColor: null, + trailing: isLoadingMore.value ? const LoadingIndicator() : null, + onTap: () async { + isLoadingMore.value = true; + await VideoController.inst.fetchYTQualities(currentTrack); + isLoadingMore.value = false; + }, + ), + ...availableVideos.map( + (element) { + final localOrCache = element.ytID == null ? lang.LOCAL : lang.CACHE; + return Obx( + () { + final currentVideo = VideoController.inst.currentVideo.value; + final isCurrent = element.path == currentVideo?.path; + return _MPQualityButton( + bgColor: isCurrent ? CurrentColor.inst.miniplayerColor.withAlpha(20) : null, + icon: Broken.video, + title: [ + "${element.height}p${element.framerateText()}", + localOrCache, + ].join(' • '), + subtitle: [ + element.sizeInBytes.fileSizeFormatted, + "${element.bitrate ~/ 1000} kb/s", + ].join(' • '), + trailing: NamidaCheckMark( + active: isCurrent, + size: 12.0, + ), + onTap: () { + VideoController.inst.playVideoCurrent(video: element, track: currentTrack); + }, + ); + }, + ); + }, + ), + const NamidaContainerDivider(height: 2.0, margin: EdgeInsets.symmetric(vertical: 6.0)), + ...ytVideos.map( + (element) { + return Obx( + () { + final currentVideo = VideoController.inst.currentVideo.value; + final cacheFile = currentVideo?.ytID == null ? null : element.getCachedFile(currentVideo!.ytID!); + final cacheExists = cacheFile != null; + return _MPQualityButton( + onTap: () async { + if (!cacheExists) await VideoController.inst.getVideoFromYoutubeAndUpdate(currentVideo?.ytID, stream: element); + VideoController.inst.playVideoCurrent(video: null, cacheIdAndPath: (currentVideo?.ytID ?? '', cacheFile?.path ?? ''), track: currentTrack); + }, + bgColor: cacheExists ? CurrentColor.inst.miniplayerColor.withAlpha(40) : null, + icon: cacheExists ? Broken.tick_circle : Broken.import, + title: "${element.resolution} • ${element.sizeInBytes?.fileSizeFormatted}", + subtitle: "${element.formatSuffix} • ${element.bitrateText}", + ); + }, + ); + }, + ), + ], + ); + }, + ), + child: Obx( + () { + final videoPlaybackEnabled = settings.enableVideoPlayback.value; + final currentVideo = VideoController.inst.currentVideo.value; + final downloadedBytes = VideoController.inst.currentDownloadedBytes.value; + final videoTotalSize = currentVideo?.sizeInBytes ?? 0; + final videoQuality = currentVideo?.height ?? 0; + final videoFramerate = currentVideo?.framerateText(30); + final markText = VideoController.inst.isNoVideosAvailable.value ? 'x' : '?'; + final fallbackQualityLabel = currentVideo?.nameInCache?.split('_').last; + final qualityText = videoQuality == 0 ? fallbackQualityLabel ?? markText : '${videoQuality}p'; + final framerateText = videoFramerate ?? ''; + return AnimatedContainer( + duration: animationDuration, + decoration: isMenuOpened.value + ? BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(24.0.multipliedRadius), + ) + : BoxDecoration( + borderRadius: BorderRadius.circular(12.0.multipliedRadius), + ), + child: TextButton( + onPressed: () async => await VideoController.inst.toggleVideoPlayback(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: context.theme.colorScheme.secondaryContainer, + shape: BoxShape.circle, + ), + child: NamidaIconButton( + horizontalPadding: 0.0, + onPressed: () { + String toPercentage(double val) => "${(val * 100).toStringAsFixed(0)}%"; + + Widget getTextWidget(IconData icon, String title, double value) { + return Row( + children: [ + Icon(icon, color: context.defaultIconColor()), + const SizedBox(width: 12.0), + Text( + title, + style: context.textTheme.displayLarge, + ), + const SizedBox(width: 8.0), + Text( + toPercentage(value), + style: context.textTheme.displayMedium, + ) + ], + ); + } + + Widget getSlider({ + double min = 0.0, + double max = 2.0, + required double value, + required void Function(double newValue)? onChanged, + }) { + return Slider.adaptive( + min: min, + max: max, + value: value.clamp(min, max), + onChanged: onChanged, + divisions: 100, + label: "${(value * 100).toStringAsFixed(0)}%", + ); + } + + NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + title: lang.CONFIGURE, + actions: [ + NamidaIconButton( + icon: Broken.refresh, + onPressed: () { + const val = 1.0; + Player.inst.setPlayerPitch(val); + Player.inst.setPlayerSpeed(val); + Player.inst.setPlayerVolume(val); + settings.save( + playerPitch: val, + playerSpeed: val, + playerVolume: val, + ); + }, + ), + NamidaButton( + text: lang.DONE, + onPressed: () { + NamidaNavigator.inst.closeDialog(); + }, + ) + ], + child: ListView( + padding: const EdgeInsets.all(12.0), + shrinkWrap: true, + children: [ + Obx(() => getTextWidget(Broken.airpods, lang.PITCH, settings.playerPitch.value)), + Obx( + () => getSlider( + value: settings.playerPitch.value, + onChanged: (value) { + Player.inst.setPlayerPitch(value); + settings.save(playerPitch: value); + }, + ), + ), + const SizedBox(height: 12.0), + Obx( + () => getTextWidget(Broken.forward, lang.SPEED, settings.playerSpeed.value), + ), + Obx( + () => getSlider( + value: settings.playerSpeed.value, + onChanged: (value) { + Player.inst.setPlayerSpeed(value); + settings.save(playerSpeed: value); + }, + ), + ), + const SizedBox(height: 12.0), + Obx( + () => + getTextWidget(settings.playerVolume.value > 0 ? Broken.volume_high : Broken.volume_slash, lang.VOLUME, settings.playerVolume.value), + ), + Obx( + () => getSlider( + max: 1.0, + value: settings.playerVolume.value, + onChanged: (value) { + Player.inst.setPlayerVolume(value); + settings.save(playerVolume: value); + }, + ), + ), + const SizedBox(height: 12.0), + ], + ), + ), + ); + }, + icon: videoPlaybackEnabled ? Broken.video : Broken.headphone, + iconSize: 18.0, + iconColor: onSecondary, + ), + ), + const SizedBox( + width: 8.0, + ), + if (!videoPlaybackEnabled) ...[ + _MPTextWidget(lang.AUDIO, color: onSecondary), + if (settings.displayAudioInfoMiniplayer.value) + Obx( + () { + final currentTrack = Player.inst.nowPlayingTrack; + return _MPTextWidget( + " • ${currentTrack.audioInfoFormattedCompact}", + color: onSecondary, + textColor: context.theme.colorScheme.onPrimaryContainer, + fontSize: 10.0, + ); + }, + ), + ], + if (videoPlaybackEnabled) ...[ + _MPTextWidget(lang.VIDEO, color: onSecondary), + qualityText == '?' && !ConnectivityController.inst.hasConnection + ? Row( + children: [ + _MPTextWidget(" • ", color: onSecondary), + Icon( + Broken.global_refresh, + size: 14.0, + color: onSecondary, + ), + ], + ) + : _MPTextWidget(" • $qualityText$framerateText", color: null, fontSize: 13.0), + if (videoTotalSize > 0) ...[ + const _MPTextWidget(" • ", color: null, fontSize: 13.0), + if (downloadedBytes != null) _MPTextWidget("${downloadedBytes.fileSizeFormatted}/", color: onSecondary, fontSize: 10.0), + _MPTextWidget(videoTotalSize.fileSizeFormatted, color: onSecondary, fontSize: 10.0), + ] + ] + ], + ), + ), + ); + }, + ), + ), ), ), ), - ), + // -- constant [4] -- buttons row + Align( + alignment: Alignment.bottomRight, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 34, + height: 34, + child: Obx( + () => IconButton( + visualDensity: VisualDensity.compact, + tooltip: settings.playerRepeatMode.value.toText().replaceFirst('_NUM_', Player.inst.numberOfRepeats.toString()), + onPressed: () { + final e = settings.playerRepeatMode.value.nextElement(RepeatMode.values); + settings.save(playerRepeatMode: e); + }, + padding: const EdgeInsets.all(2.0), + icon: Stack( + alignment: Alignment.center, + children: [ + Icon( + settings.playerRepeatMode.value.toIcon(), + size: 20.0, + color: context.theme.colorScheme.onSecondaryContainer, + ), + if (settings.playerRepeatMode.value == RepeatMode.forNtimes) + Text( + Player.inst.numberOfRepeats.toString(), + style: context.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), + ), + ], + ), + ), + ), + ), + SizedBox( + width: 34, + height: 34, + child: GestureDetector( + onLongPress: () { + showLRCSetDialog(Player.inst.nowPlayingTrack, CurrentColor.inst.miniplayerColor); + }, + child: IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + settings.save(enableLyrics: !settings.enableLyrics.value); + Lyrics.inst.updateLyrics(Player.inst.nowPlayingTrack); + }, + padding: const EdgeInsets.all(2.0), + icon: Obx( + () => settings.enableLyrics.value + ? Lyrics.inst.currentLyricsText.value == '' && Lyrics.inst.currentLyricsLRC.value == null + ? StackedIcon( + baseIcon: Broken.document, + secondaryText: !Lyrics.inst.lyricsCanBeAvailable.value ? 'x' : '?', + iconSize: 20.0, + blurRadius: 6.0, + baseIconColor: context.theme.colorScheme.onSecondaryContainer, + ) + : Icon( + Broken.document, + size: 20.0, + color: context.theme.colorScheme.onSecondaryContainer, + ) + : Icon( + Broken.card_slash, + size: 20.0, + color: context.theme.colorScheme.onSecondaryContainer, + ), + ), + ), + ), + ), + SizedBox( + width: 34, + height: 34, + child: IconButton( + tooltip: lang.QUEUE, + visualDensity: VisualDensity.compact, + onPressed: MiniPlayerController.inst.snapToQueue, + padding: const EdgeInsets.all(2.0), + icon: Icon( + Broken.row_vertical, + size: 19.0, + color: context.theme.colorScheme.onSecondaryContainer, + ), + ), + ), + const SizedBox(width: 10.0), + ], + ), + ), + ), + ), + + // constants [5] -- waveform + const Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: WaveformMiniplayer(), + ), + ), + ], builder: ( - onSecondary, maxOffset, bounceUp, bounceDown, @@ -320,7 +862,7 @@ class _NamidaMiniPlayerState extends State { panelHeight, miniplayerbottomnavheight, bottomOffset, - constantChild, + constantChildren, ) { return Obx( () { @@ -385,11 +927,11 @@ class _NamidaMiniPlayerState extends State { final w = Player.inst.nowPlayingPosition / currentDurationInMS; return Container( height: 2 * (1 - cp), - width: w > 0 ? ((Get.width * w) * 0.9) : 0, + width: w > 0 ? ((context.width * w) * 0.9) : 0, margin: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration( color: CurrentColor.inst.miniplayerColor, - borderRadius: BorderRadius.circular(50), + borderRadius: const BorderRadius.all(Radius.circular(50)), // color: Color.alphaBlend(context.theme.colorScheme.onBackground.withAlpha(40), CurrentColor.inst.miniplayerColor) // .withOpacity(velpy(a: .3, b: .22, c: icp)), ), @@ -434,9 +976,6 @@ class _NamidaMiniPlayerState extends State { ], ), ), - // if (settings.enablePartyModeInMiniplayer.value) ...[ - - // ], /// Top Row if (rcp > 0.0) @@ -449,58 +988,7 @@ class _NamidaMiniPlayerState extends State { child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: MiniPlayerController.inst.snapToMini, - icon: Icon(Broken.arrow_down_2, color: onSecondary), - iconSize: 22.0, - ), - Expanded( - child: NamidaInkWell( - borderRadius: 14.0, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - onTap: () => NamidaOnTaps.inst.onAlbumTap(currentTrack.albumIdentifier), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${currentIndex + 1}/${Player.inst.currentQueue.length}", - style: TextStyle( - color: onSecondary.withOpacity(.8), - fontSize: 12.0.multipliedFontScale, - fontWeight: FontWeight.w500, - ), - ), - Text( - currentTrack.album, - textAlign: TextAlign.center, - maxLines: 1, - softWrap: false, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0.multipliedFontScale, color: onSecondary.withOpacity(.9)), - ), - ], - ), - ), - ), - IconButton( - onPressed: () { - NamidaDialogs.inst.showTrackDialog(currentTrack, source: QueueSource.playerQueue); - }, - icon: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: context.theme.colorScheme.secondary.withOpacity(.2), - shape: BoxShape.circle, - ), - child: Icon(Broken.more, color: onSecondary), - ), - iconSize: 22.0, - ), - ], - ), + child: constantChildren[1], ), ), ), @@ -533,69 +1021,7 @@ class _NamidaMiniPlayerState extends State { opacity: fastOpacity, child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.0 * (16 * (!bounceDown ? icp : 0.0) + 1)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () => Player.inst.seekSecondsBackward(), - onLongPress: () => Player.inst.seek(Duration.zero), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () { - final seek = MiniPlayerController.inst.seekValue.value; - final diffInMs = seek - Player.inst.nowPlayingPosition; - final plusOrMinus = diffInMs < 0 ? '-' : '+'; - final seekText = seek == 0 ? '00:00' : diffInMs.abs().milliSecondsLabel; - return Text( - "$plusOrMinus$seekText", - style: context.textTheme.displaySmall?.copyWith(fontSize: 10.0.multipliedFontScale), - ).animateEntrance( - showWhen: seek != 0, - durationMS: 700, - allCurves: Curves.easeInOutQuart, - ); - }, - ), - NamidaHero( - tag: 'MINIPLAYER_POSITION', - child: Obx( - () => Text( - Player.inst.nowPlayingPosition.milliSecondsLabel, - style: context.textTheme.displaySmall, - ), - ), - ), - ], - ), - ), - ), - GestureDetector( - onTap: () => Player.inst.seekSecondsForward(), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: NamidaHero( - tag: 'MINIPLAYER_DURATION', - child: Obx( - () { - final displayRemaining = settings.displayRemainingDurInsteadOfTotal.value; - final toSubtract = displayRemaining ? Player.inst.nowPlayingPosition : 0; - final msToDisplay = currentDurationInMS - toSubtract; - final prefix = displayRemaining ? '-' : ''; - return Text( - "$prefix ${msToDisplay.milliSecondsLabel}", - style: context.textTheme.displaySmall, - ); - }, - ), - ), - ), - ), - ], - ), + child: constantChildren[2], ), ), Padding( @@ -710,302 +1136,7 @@ class _NamidaMiniPlayerState extends State { opacity: opacity, child: Transform.translate( offset: Offset(0, -100 * ip), - child: Align( - alignment: Alignment.bottomLeft, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), - child: FocusedMenuHolder( - menuOpenAlignment: Alignment.bottomLeft, - bottomOffsetHeight: 12.0, - leftOffsetHeight: 4.0, - onMenuOpen: () { - if (settings.enableVideoPlayback.value) { - isMenuOpened.value = true; - return true; - } else { - ScrollSearchController.inst.unfocusKeyboard(); - NamidaNavigator.inst.navigateDialog(dialog: const Dialog(child: PlaybackSettings(isInDialog: true))); - return false; - } - }, - onMenuClose: () => isMenuOpened.value = false, - blurSize: 2.0, - duration: animationDuration, - animateMenuItems: false, - menuWidth: context.width * 0.5, - menuBoxDecoration: BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12.0.multipliedRadius), - ), - menuWidget: Obx( - () { - final availableVideos = VideoController.inst.currentPossibleVideos; - final ytVideos = VideoController.inst.currentYTQualities.where((s) => s.formatSuffix != 'webm'); - return ListView( - padding: const EdgeInsets.symmetric(vertical: 12.0), - children: [ - _MPQualityButton( - title: lang.CHECK_FOR_MORE, - icon: Broken.chart, - bgColor: null, - trailing: isLoadingMore.value ? const LoadingIndicator() : null, - onTap: () async { - isLoadingMore.value = true; - await VideoController.inst.fetchYTQualities(currentTrack); - isLoadingMore.value = false; - }, - ), - ...availableVideos.map( - (element) { - final localOrCache = element.ytID == null ? lang.LOCAL : lang.CACHE; - return Obx( - () { - final currentVideo = VideoController.inst.currentVideo.value; - final isCurrent = element.path == currentVideo?.path; - return _MPQualityButton( - bgColor: isCurrent ? CurrentColor.inst.miniplayerColor.withAlpha(20) : null, - icon: Broken.video, - title: [ - "${element.height}p${element.framerateText()}", - localOrCache, - ].join(' • '), - subtitle: [ - element.sizeInBytes.fileSizeFormatted, - "${element.bitrate ~/ 1000} kb/s", - ].join(' • '), - trailing: NamidaCheckMark( - active: isCurrent, - size: 12.0, - ), - onTap: () { - VideoController.inst.playVideoCurrent(video: element, track: currentTrack); - }, - ); - }, - ); - }, - ), - const NamidaContainerDivider(height: 2.0, margin: EdgeInsets.symmetric(vertical: 6.0)), - ...ytVideos.map( - (element) { - return Obx( - () { - final currentVideo = VideoController.inst.currentVideo.value; - final cacheFile = currentVideo?.ytID == null ? null : element.getCachedFile(currentVideo!.ytID!); - final cacheExists = cacheFile != null; - return _MPQualityButton( - onTap: () async { - if (!cacheExists) await VideoController.inst.getVideoFromYoutubeAndUpdate(currentVideo?.ytID, stream: element); - VideoController.inst - .playVideoCurrent(video: null, cacheIdAndPath: (currentVideo?.ytID ?? '', cacheFile?.path ?? ''), track: currentTrack); - }, - bgColor: cacheExists ? CurrentColor.inst.miniplayerColor.withAlpha(40) : null, - icon: cacheExists ? Broken.tick_circle : Broken.import, - title: "${element.resolution} • ${element.sizeInBytes?.fileSizeFormatted}", - subtitle: "${element.formatSuffix} • ${element.bitrateText}", - ); - }, - ); - }, - ), - ], - ); - }, - ), - child: Obx( - () { - final videoPlaybackEnabled = settings.enableVideoPlayback.value; - final currentVideo = VideoController.inst.currentVideo.value; - final downloadedBytes = VideoController.inst.currentDownloadedBytes.value; - final videoTotalSize = currentVideo?.sizeInBytes ?? 0; - final videoQuality = currentVideo?.height ?? 0; - final videoFramerate = currentVideo?.framerateText(30); - final markText = VideoController.inst.isNoVideosAvailable.value ? 'x' : '?'; - final fallbackQualityLabel = currentVideo?.nameInCache?.split('_').last; - final qualityText = videoQuality == 0 ? fallbackQualityLabel ?? markText : '${videoQuality}p'; - final framerateText = videoFramerate ?? ''; - return AnimatedContainer( - duration: animationDuration, - decoration: isMenuOpened.value - ? BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(24.0.multipliedRadius), - ) - : BoxDecoration( - borderRadius: BorderRadius.circular(12.0.multipliedRadius), - ), - child: TextButton( - onPressed: () async => await VideoController.inst.toggleVideoPlayback(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - color: context.theme.colorScheme.secondaryContainer, - shape: BoxShape.circle, - ), - child: NamidaIconButton( - horizontalPadding: 0.0, - onPressed: () { - String toPercentage(double val) => "${(val * 100).toStringAsFixed(0)}%"; - - Widget getTextWidget(IconData icon, String title, double value) { - return Row( - children: [ - Icon(icon, color: context.defaultIconColor()), - const SizedBox(width: 12.0), - Text( - title, - style: context.textTheme.displayLarge, - ), - const SizedBox(width: 8.0), - Text( - toPercentage(value), - style: context.textTheme.displayMedium, - ) - ], - ); - } - - Widget getSlider({ - double min = 0.0, - double max = 2.0, - required double value, - required void Function(double newValue)? onChanged, - }) { - return Slider.adaptive( - min: min, - max: max, - value: value.clamp(min, max), - onChanged: onChanged, - divisions: 100, - label: "${(value * 100).toStringAsFixed(0)}%", - ); - } - - NamidaNavigator.inst.navigateDialog( - dialog: CustomBlurryDialog( - title: lang.CONFIGURE, - actions: [ - NamidaIconButton( - icon: Broken.refresh, - onPressed: () { - const val = 1.0; - Player.inst.setPlayerPitch(val); - Player.inst.setPlayerSpeed(val); - Player.inst.setPlayerVolume(val); - settings.save( - playerPitch: val, - playerSpeed: val, - playerVolume: val, - ); - }, - ), - NamidaButton( - text: lang.DONE, - onPressed: () { - NamidaNavigator.inst.closeDialog(); - }, - ) - ], - child: ListView( - padding: const EdgeInsets.all(12.0), - shrinkWrap: true, - children: [ - Obx(() => getTextWidget(Broken.airpods, lang.PITCH, settings.playerPitch.value)), - Obx( - () => getSlider( - value: settings.playerPitch.value, - onChanged: (value) { - Player.inst.setPlayerPitch(value); - settings.save(playerPitch: value); - }, - ), - ), - const SizedBox(height: 12.0), - Obx( - () => getTextWidget(Broken.forward, lang.SPEED, settings.playerSpeed.value), - ), - Obx( - () => getSlider( - value: settings.playerSpeed.value, - onChanged: (value) { - Player.inst.setPlayerSpeed(value); - settings.save(playerSpeed: value); - }, - ), - ), - const SizedBox(height: 12.0), - Obx( - () => getTextWidget( - settings.playerVolume.value > 0 ? Broken.volume_high : Broken.volume_slash, lang.VOLUME, settings.playerVolume.value), - ), - Obx( - () => getSlider( - max: 1.0, - value: settings.playerVolume.value, - onChanged: (value) { - Player.inst.setPlayerVolume(value); - settings.save(playerVolume: value); - }, - ), - ), - const SizedBox(height: 12.0), - ], - ), - ), - ); - }, - icon: videoPlaybackEnabled ? Broken.video : Broken.headphone, - iconSize: 18.0, - iconColor: onSecondary, - ), - ), - const SizedBox( - width: 8.0, - ), - if (!videoPlaybackEnabled) ...[ - _MPTextWidget(lang.AUDIO, color: onSecondary), - if (settings.displayAudioInfoMiniplayer.value) - _MPTextWidget( - " • ${currentTrack.audioInfoFormattedCompact}", - color: onSecondary, - textColor: context.theme.colorScheme.onPrimaryContainer, - fontSize: 10.0, - ), - ], - if (videoPlaybackEnabled) ...[ - _MPTextWidget(lang.VIDEO, color: onSecondary), - qualityText == '?' && !ConnectivityController.inst.hasConnection - ? Row( - children: [ - _MPTextWidget(" • ", color: onSecondary), - Icon( - Broken.global_refresh, - size: 14.0, - color: onSecondary, - ), - ], - ) - : _MPTextWidget(" • $qualityText$framerateText", color: null, fontSize: 13.0), - if (videoTotalSize > 0) ...[ - const _MPTextWidget(" • ", color: null, fontSize: 13.0), - if (downloadedBytes != null) _MPTextWidget("${downloadedBytes.fileSizeFormatted}/", color: onSecondary, fontSize: 10.0), - _MPTextWidget(videoTotalSize.fileSizeFormatted, color: onSecondary, fontSize: 10.0), - ] - ] - ], - ), - ), - ); - }, - ), - ), - ), - ), - ), + child: constantChildren[3], ), ), @@ -1017,103 +1148,7 @@ class _NamidaMiniPlayerState extends State { opacity: opacity, child: Transform.translate( offset: Offset(0, -100 * ip), - child: Align( - alignment: Alignment.bottomRight, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 18.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 34, - height: 34, - child: Obx( - () => IconButton( - visualDensity: VisualDensity.compact, - tooltip: settings.playerRepeatMode.value.toText().replaceFirst('_NUM_', Player.inst.numberOfRepeats.toString()), - onPressed: () { - final e = settings.playerRepeatMode.value.nextElement(RepeatMode.values); - settings.save(playerRepeatMode: e); - }, - padding: const EdgeInsets.all(2.0), - icon: Stack( - alignment: Alignment.center, - children: [ - Icon( - settings.playerRepeatMode.value.toIcon(), - size: 20.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), - if (settings.playerRepeatMode.value == RepeatMode.forNtimes) - Text( - Player.inst.numberOfRepeats.toString(), - style: context.textTheme.displaySmall?.copyWith(color: context.theme.colorScheme.onSecondaryContainer), - ), - ], - ), - ), - ), - ), - SizedBox( - width: 34, - height: 34, - child: GestureDetector( - onLongPress: () { - showLRCSetDialog(currentTrack, CurrentColor.inst.miniplayerColor); - }, - child: IconButton( - visualDensity: VisualDensity.compact, - onPressed: () { - settings.save(enableLyrics: !settings.enableLyrics.value); - Lyrics.inst.updateLyrics(currentTrack); - }, - padding: const EdgeInsets.all(2.0), - icon: Obx( - () => settings.enableLyrics.value - ? Lyrics.inst.currentLyricsText.value == '' && Lyrics.inst.currentLyricsLRC.value == null - ? StackedIcon( - baseIcon: Broken.document, - secondaryText: !Lyrics.inst.lyricsCanBeAvailable.value ? 'x' : '?', - iconSize: 20.0, - blurRadius: 6.0, - baseIconColor: context.theme.colorScheme.onSecondaryContainer, - ) - : Icon( - Broken.document, - size: 20.0, - color: context.theme.colorScheme.onSecondaryContainer, - ) - : Icon( - Broken.card_slash, - size: 20.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), - ), - ), - ), - ), - SizedBox( - width: 34, - height: 34, - child: IconButton( - tooltip: lang.QUEUE, - visualDensity: VisualDensity.compact, - onPressed: MiniPlayerController.inst.snapToQueue, - padding: const EdgeInsets.all(2.0), - icon: Icon( - Broken.row_vertical, - size: 19.0, - color: context.theme.colorScheme.onSecondaryContainer, - ), - ), - ), - const SizedBox(width: 10.0), - ], - ), - ), - ), - ), + child: constantChildren[4], ), ), ), @@ -1274,13 +1309,7 @@ class _NamidaMiniPlayerState extends State { : (1 - bp) : 0.0)) * 0.4)), - child: const Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: WaveformMiniplayer(), - ), - ), + child: constantChildren[5], ), ), @@ -1289,7 +1318,7 @@ class _NamidaMiniPlayerState extends State { opacity: qp.clamp(0.0, 1.0), child: Transform.translate( offset: Offset(0, (1 - qp) * maxOffset * 0.8), - child: constantChild, + child: constantChildren[0], ), ), ], diff --git a/lib/packages/miniplayer_raw.dart b/lib/packages/miniplayer_raw.dart index 0e788c3c..62d519e9 100644 --- a/lib/packages/miniplayer_raw.dart +++ b/lib/packages/miniplayer_raw.dart @@ -3,14 +3,12 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - import 'package:namida/controller/miniplayer_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; typedef MiniplayerBuilderCallback = Widget Function( - Color onSecondary, double maxOffset, bool bounceUp, bool bounceDown, @@ -38,7 +36,7 @@ typedef MiniplayerBuilderCallback = Widget Function( double panelHeight, double miniplayerbottomnavheight, double bottomOffset, - Widget? constantChild, + List constantChildren, ); class MiniplayerRaw extends StatelessWidget { @@ -46,7 +44,7 @@ class MiniplayerRaw extends StatelessWidget { final double topBorderRadius; final double bottomBorderRadius; final bool enableHorizontalGestures; - final Widget? constantChild; + final List constantChildren; const MiniplayerRaw({ super.key, @@ -54,15 +52,14 @@ class MiniplayerRaw extends StatelessWidget { this.topBorderRadius = 20.0, this.bottomBorderRadius = 20.0, this.enableHorizontalGestures = true, - this.constantChild, + this.constantChildren = const [], }); @override Widget build(BuildContext context) { - final child = AnimatedBuilder( + final child = AnimatedBuilderMulti( animation: MiniPlayerController.inst.animation, builder: (context, child) { - final Color onSecondary = context.theme.colorScheme.onSecondaryContainer; final maxOffset = MiniPlayerController.inst.maxOffset; final bounceUp = MiniPlayerController.inst.bounceUp; final bounceDown = MiniPlayerController.inst.bounceDown; @@ -110,7 +107,6 @@ class MiniplayerRaw extends StatelessWidget { final double bottomOffset = (-miniplayerbottomnavheight * icp + p.clamp(-1, 0) * -200) - (bottomInset * icp); return builder( - onSecondary, maxOffset, bounceUp, bounceDown, @@ -141,7 +137,7 @@ class MiniplayerRaw extends StatelessWidget { child, ); }, - child: constantChild, + children: constantChildren, ); return WillPopScope( onWillPop: MiniPlayerController.inst.onWillPop, diff --git a/lib/packages/mp.dart b/lib/packages/mp.dart index 62c4cae3..fb066a7a 100644 --- a/lib/packages/mp.dart +++ b/lib/packages/mp.dart @@ -11,7 +11,7 @@ bool _wasExpanded = false; class NamidaYTMiniplayer extends StatefulWidget { final double minHeight, maxHeight, bottomMargin; - final Widget Function(double height, double percentage, Widget? constantChild) builder; + final Widget Function(double height, double percentage, List constantChildren) builder; final Decoration? decoration; final void Function(double percentage)? onHeightChange; final void Function(double dismissPercentage)? onDismissing; @@ -19,7 +19,7 @@ class NamidaYTMiniplayer extends StatefulWidget { final Curve curve; final AnimationController? animationController; final void Function()? onDismiss; - final Widget? constantChild; + final List constantChildren; const NamidaYTMiniplayer({ super.key, @@ -34,7 +34,7 @@ class NamidaYTMiniplayer extends StatefulWidget { this.curve = Curves.decelerate, this.animationController, this.onDismiss, - this.constantChild, + required this.constantChildren, }); @override @@ -115,9 +115,9 @@ class NamidaYTMiniplayerState extends State with SingleTicke @override Widget build(BuildContext context) { - return AnimatedBuilder( + return AnimatedBuilderMulti( animation: controller, - builder: (context, child) { + builder: (context, children) { final percentage = this.percentage; return Stack( alignment: Alignment.bottomCenter, @@ -171,7 +171,7 @@ class NamidaYTMiniplayerState extends State with SingleTicke child: Container( height: controller.value, decoration: widget.decoration, - child: widget.builder(controller.value, percentage, child), + child: widget.builder(controller.value, percentage, children), ), ), ), @@ -181,7 +181,7 @@ class NamidaYTMiniplayerState extends State with SingleTicke ], ); }, - child: widget.constantChild, + children: widget.constantChildren, ); } } diff --git a/lib/ui/widgets/custom_widgets.dart b/lib/ui/widgets/custom_widgets.dart index d852b01a..80518673 100644 --- a/lib/ui/widgets/custom_widgets.dart +++ b/lib/ui/widgets/custom_widgets.dart @@ -3218,3 +3218,49 @@ class _VisibilityDetectorState extends State { @override Widget build(BuildContext context) => widget.child; } + +class AnimatedBuilderMulti extends StatelessWidget { + final Listenable animation; + final List children; + final Widget Function(BuildContext context, List children) builder; + + const AnimatedBuilderMulti({ + super.key, + required this.animation, + required this.children, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + child: AnimatedBuilderChildren(items: children), + builder: (context, child) => builder(context, (child as AnimatedBuilderChildren).items), + ); + } +} + +class AnimatedBuilderChildren extends Widget { + final List items; + const AnimatedBuilderChildren({super.key, required this.items}); + + @override + Element createElement() => _DummyElement(this); +} + +class _DummyElement extends Element { + _DummyElement(super.widget); + + @override + void mount(Element? parent, dynamic newSlot) { + super.mount(parent, newSlot); + } + + @override + bool get debugDoingBuild => false; + + @override + // ignore: must_call_super + void performRebuild() {} +} diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 9f173984..0b17ebb8 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -149,689 +149,786 @@ class YoutubeMiniPlayer extends StatelessWidget { Player.inst.setPlayerVolume(dismissPercentage.clamp(0.0, settings.playerVolume.value)); }, onHeightChange: (percentage) => MiniPlayerController.inst.animateMiniplayer(percentage), - constantChild: Stack( - alignment: Alignment.bottomCenter, - children: [ - // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== - // opacity: (percentage * 4 - 3).withMinimum(0), - Listener( - key: Key("${currentId}_body_listener"), - onPointerDown: (event) => YoutubeController.inst.cancelDimTimer(), - onPointerUp: (event) => YoutubeController.inst.startDimTimer(), - child: Navigator( - key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, - requestFocus: false, - onPopPage: (route, result) => false, - restorationScopeId: currentId, - pages: [ - MaterialPage( - maintainState: true, - child: LazyLoadListView( - key: Key("${currentId}_body_lazy_load_list"), - onReachingEnd: () async => await YoutubeController.inst.updateCurrentComments(currentId, fetchNextOnly: true), - extend: 400, - scrollController: YoutubeController.inst.scrollController, - listview: (controller) => ObxValue( - (isTitleExpanded) => Stack( - key: Key("${currentId}_body_stack"), - children: [ - CustomScrollView( - // key: PageStorageKey(currentId), // duplicate errors - physics: const ClampingScrollPhysics(), - controller: controller, - slivers: [ - // --START-- title & subtitle - SliverToBoxAdapter( - key: Key("${currentId}_title"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: videoInfo == 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.color.withAlpha(40), context.theme.colorScheme.onBackground), - collapsedTextColor: context.theme.colorScheme.onBackground, - iconColor: Color.alphaBlend(CurrentColor.inst.color.withAlpha(40), context.theme.colorScheme.onBackground), - collapsedIconColor: context.theme.colorScheme.onBackground, - childrenPadding: const EdgeInsets.all(18.0), - onExpansionChanged: (value) => isTitleExpanded.value = value, - trailing: Obx( - () { - final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (videoListens.isNotEmpty) - NamidaInkWell( - borderRadius: 6.0, - bgColor: CurrentColor.inst.color.withOpacity(0.7), - onTap: () { - showVideoListensDialog(currentId); - }, - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), - child: Text( - videoListens.length.formatDecimal(), - style: context.textTheme.displaySmall?.copyWith( - color: Colors.white.withOpacity(0.6), + constantChildren: [ + // constant [0] + // ==== MiniPlayer Body, contains title, description, comments, ..etc. ==== + Stack( + alignment: Alignment.bottomCenter, + children: [ + // opacity: (percentage * 4 - 3).withMinimum(0), + Listener( + key: Key("${currentId}_body_listener"), + onPointerDown: (event) => YoutubeController.inst.cancelDimTimer(), + onPointerUp: (event) => YoutubeController.inst.startDimTimer(), + child: Navigator( + key: NamidaNavigator.inst.ytMiniplayerCommentsPageKey, + requestFocus: false, + onPopPage: (route, result) => false, + restorationScopeId: currentId, + pages: [ + MaterialPage( + maintainState: true, + child: LazyLoadListView( + key: Key("${currentId}_body_lazy_load_list"), + onReachingEnd: () async => await YoutubeController.inst.updateCurrentComments(currentId, fetchNextOnly: true), + extend: 400, + scrollController: YoutubeController.inst.scrollController, + listview: (controller) => ObxValue( + (isTitleExpanded) => Stack( + key: Key("${currentId}_body_stack"), + children: [ + CustomScrollView( + // key: PageStorageKey(currentId), // duplicate errors + physics: const ClampingScrollPhysics(), + controller: controller, + slivers: [ + // --START-- title & subtitle + SliverToBoxAdapter( + key: Key("${currentId}_title"), + child: ShimmerWrapper( + shimmerDurationMS: 550, + shimmerDelayMS: 250, + shimmerEnabled: videoInfo == 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.color.withAlpha(40), context.theme.colorScheme.onBackground), + collapsedTextColor: context.theme.colorScheme.onBackground, + iconColor: Color.alphaBlend(CurrentColor.inst.color.withAlpha(40), context.theme.colorScheme.onBackground), + collapsedIconColor: context.theme.colorScheme.onBackground, + childrenPadding: const EdgeInsets.all(18.0), + onExpansionChanged: (value) => isTitleExpanded.value = value, + trailing: Obx( + () { + final videoListens = YoutubeHistoryController.inst.topTracksMapListens[currentId] ?? []; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (videoListens.isNotEmpty) + NamidaInkWell( + borderRadius: 6.0, + bgColor: CurrentColor.inst.color.withOpacity(0.7), + onTap: () { + showVideoListensDialog(currentId); + }, + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), + child: Text( + videoListens.length.formatDecimal(), + style: context.textTheme.displaySmall?.copyWith( + color: Colors.white.withOpacity(0.6), + ), ), ), + const SizedBox(width: 8.0), + const Icon( + Broken.arrow_down_2, + size: 20.0, ), - const SizedBox(width: 8.0), - const Icon( - Broken.arrow_down_2, - size: 20.0, + ], + ); + }, + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NamidaDummyContainer( + width: context.width * 0.8, + height: 24.0, + borderRadius: 6.0, + shimmerEnabled: videoInfo == null, + child: Text( + videoInfo?.name ?? '', + maxLines: isTitleExpanded.value ? 6 : 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.displayLarge, ), - ], - ); - }, - ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - width: context.width * 0.8, - height: 24.0, - borderRadius: 6.0, - shimmerEnabled: videoInfo == null, - child: Text( - videoInfo?.name ?? '', - maxLines: isTitleExpanded.value ? 6 : 2, - overflow: TextOverflow.ellipsis, - style: context.textTheme.displayLarge, ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - width: context.width * 0.7, - height: 12.0, - shimmerEnabled: videoInfo == null, - child: () { - final expandedDate = isTitleExpanded.value ? uploadDate : null; - final collapsedDate = isTitleExpanded.value ? null : uploadDateAgo; - return Text( - [ - if (videoViewCount != null) - "${videoViewCount.formatDecimalShort(isTitleExpanded.value)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", - if (expandedDate != null) expandedDate, - if (collapsedDate != null) collapsedDate, - ].join(' • '), - style: context.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w500), - ); - }(), - ), + const SizedBox(height: 4.0), + NamidaDummyContainer( + width: context.width * 0.7, + height: 12.0, + shimmerEnabled: videoInfo == null, + child: () { + final expandedDate = isTitleExpanded.value ? uploadDate : null; + final collapsedDate = isTitleExpanded.value ? null : uploadDateAgo; + return Text( + [ + if (videoViewCount != null) + "${videoViewCount.formatDecimalShort(isTitleExpanded.value)} ${videoViewCount == 0 ? lang.VIEW : lang.VIEWS}", + if (expandedDate != null) expandedDate, + if (collapsedDate != null) collapsedDate, + ].join(' • '), + style: context.textTheme.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: videoInfo == null, - child: SizedBox( - width: context.width, - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - children: [ - const SizedBox(width: 4.0), - SmallYTActionButton( - title: videoInfo == null - ? null - : videoLikeCount < 1 - ? lang.LIKE - : videoLikeCount.formatDecimalShort(isTitleExpanded.value), - icon: Broken.like_1, - smallIconWidget: FittedBox( - child: NamidaRawLikeButton( - likedIcon: Broken.like_filled, - normalIcon: Broken.like_1, - disabledColor: context.theme.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: videoInfo == null, + child: SizedBox( + width: context.width, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + const SizedBox(width: 4.0), + SmallYTActionButton( + title: videoInfo == null + ? null + : videoLikeCount < 1 + ? lang.LIKE + : videoLikeCount.formatDecimalShort(isTitleExpanded.value), + icon: Broken.like_1, + smallIconWidget: FittedBox( + child: NamidaRawLikeButton( + likedIcon: Broken.like_filled, + normalIcon: Broken.like_1, + disabledColor: context.theme.iconTheme.color, + isLiked: isUserLiked, + onTap: (isLiked) async { + YoutubePlaylistController.inst.favouriteButtonOnPressed(currentId); + }, + ), + ), + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded.value) ?? '?', + icon: Broken.dislike, + onPressed: () {}, + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.SHARE, + icon: Broken.share, + onPressed: () { + final url = videoInfo?.url; + if (url != null) Share.share(url); + }, + ), + const SizedBox(width: 4.0), + SmallYTActionButton( + title: lang.REFRESH, + icon: Broken.refresh, + onPressed: () async => await YoutubeController.inst.updateVideoDetails(currentId, forceRequest: true), + ), + const SizedBox(width: 4.0), + Obx( + () { + final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; + final audioPerc = audioProgress == null + ? null + : "${lang.AUDIO} ${(audioProgress.progress / audioProgress.totalProgress * 100).toStringAsFixed(0)}%"; + final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; + final videoPerc = videoProgress == null + ? null + : "${lang.VIDEO} ${(videoProgress.progress / videoProgress.totalProgress * 100).toStringAsFixed(0)}%"; + + final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; + + final wasDownloading = videoPerc != null || audioPerc != null; + final icon = (wasDownloading && !isDownloading) + ? Broken.play_circle + : wasDownloading + ? Broken.pause_circle + : downloadedFileExists + ? Broken.tick_circle + : Broken.import; + return SmallYTActionButton( + titleWidget: videoPerc == null && audioPerc == null && isDownloading ? const LoadingIndicator() : null, + title: videoPerc ?? audioPerc ?? 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: videoInfo?.name ?? '', }, ), ), + const SizedBox(width: 4.0), + ], + ), + ), + ), + ), + 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: Row( + children: [ + const SizedBox(width: 18.0), + NamidaDummyContainer( + width: 42.0, + height: 42.0, + borderRadius: 100.0, + shimmerEnabled: channelThumbnail == null, + child: YoutubeThumbnail( + key: Key(channelThumbnail ?? ''), + isImportantInCache: true, + channelUrl: channelThumbnail ?? '', + channelIDForHQImage: channelIDOrURL ?? '', + width: 42.0, + height: 42.0, + isCircle: true, + ), ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: (videoDislikeCount ?? 0) < 1 ? lang.DISLIKE : videoDislikeCount?.formatDecimalShort(isTitleExpanded.value) ?? '?', - icon: Broken.dislike, - onPressed: () {}, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SHARE, - icon: Broken.share, - onPressed: () { - final url = videoInfo?.url; - if (url != null) Share.share(url); - }, - ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.REFRESH, - icon: Broken.refresh, - onPressed: () async => await YoutubeController.inst.updateVideoDetails(currentId, forceRequest: 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: context.textTheme.displayMedium?.copyWith( + fontSize: 13.5.multipliedFontScale, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + 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: Text( + [ + channelSubs?.formatDecimalShort(isTitleExpanded.value) ?? '?', + (channelSubs ?? 0) < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS + ].join(' '), + style: context.textTheme.displaySmall?.copyWith( + fontSize: 12.0.multipliedFontScale, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), ), - const SizedBox(width: 4.0), + const SizedBox(width: 12.0), Obx( () { - final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; - final audioPerc = audioProgress == null - ? null - : "${lang.AUDIO} ${(audioProgress.progress / audioProgress.totalProgress * 100).toStringAsFixed(0)}%"; - final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; - final videoPerc = videoProgress == null - ? null - : "${lang.VIDEO} ${(videoProgress.progress / videoProgress.totalProgress * 100).toStringAsFixed(0)}%"; - - final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; - - final wasDownloading = videoPerc != null || audioPerc != null; - final icon = (wasDownloading && !isDownloading) - ? Broken.play_circle - : wasDownloading - ? Broken.pause_circle - : downloadedFileExists - ? Broken.tick_circle - : Broken.import; - return SmallYTActionButton( - titleWidget: videoPerc == null && audioPerc == null && isDownloading ? const LoadingIndicator() : null, - title: videoPerc ?? audioPerc ?? lang.DOWNLOAD, - icon: icon, - onLongPress: () async => await showDownloadVideoBottomSheet(videoId: currentId), + final channelID = YoutubeSubscriptionsController.inst.idOrUrlToChannelID(channelIDOrURL); + final subscribed = YoutubeSubscriptionsController.inst.subscribedChannels[channelID]?.subscribed ?? false; + return TextButton( + style: TextButton.styleFrom( + foregroundColor: Color.alphaBlend(Colors.grey.withOpacity(subscribed ? 0.6 : 0.0), context.theme.colorScheme.primary), + ), + child: Row( + children: [ + Icon(subscribed ? Broken.tick_square : Broken.video, size: 20.0), + const SizedBox(width: 8.0), + Text( + subscribed ? lang.SUBSCRIBED : lang.SUBSCRIBE, + ), + ], + ), 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); + if (channelIDOrURL != null) { + await YoutubeSubscriptionsController.inst.changeChannelStatus(channelIDOrURL, null); } }, ); }, ), - const SizedBox(width: 4.0), - SmallYTActionButton( - title: lang.SAVE, - icon: Broken.music_playlist, - onPressed: () => showAddToPlaylistSheet( - ids: [currentId], - idsNamesLookup: { - currentId: videoInfo?.name ?? '', - }, - ), - ), - const SizedBox(width: 4.0), + const SizedBox(width: 20.0), ], ), ), ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 24.0)), - // --END- buttons + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + // --END-- channel - // --START- channel - SliverToBoxAdapter( - key: Key("${currentId}_channel"), - child: ShimmerWrapper( - shimmerDurationMS: 550, - shimmerDelayMS: 250, - shimmerEnabled: channelName == null || channelThumbnail == null || channelSubs == null, - child: Row( - children: [ - const SizedBox(width: 18.0), - NamidaDummyContainer( - width: 42.0, - height: 42.0, - borderRadius: 100.0, - shimmerEnabled: channelThumbnail == null, - child: YoutubeThumbnail( - key: Key(channelThumbnail ?? ''), - isImportantInCache: true, - channelUrl: channelThumbnail ?? '', - channelIDForHQImage: channelIDOrURL ?? '', - 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( + // --SRART-- top comments + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), + Obx( + () { + if (!settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; + final comments = YoutubeController.inst.currentComments; + return SliverToBoxAdapter( + child: comments.isEmpty + ? const SizedBox() + : NamidaInkWell( + key: Key("${currentId}_top_comments_highlight"), + bgColor: Color.alphaBlend(context.theme.scaffoldBackgroundColor.withOpacity(0.4), context.theme.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?.push( + GetPageRoute( + page: () => const YTMiniplayerCommentsSubpage(), + transition: Transition.cupertino, + ), + ); + }, + child: Column( children: [ - NamidaDummyContainer( - width: 114.0, - height: 12.0, - borderRadius: 4.0, - shimmerEnabled: channelName == null, - child: Text( - channelName ?? '', - style: context.textTheme.displayMedium?.copyWith( - fontSize: 13.5.multipliedFontScale, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon( + Broken.document, + size: 16.0, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - ), + const SizedBox(width: 8.0), + Text( + [ + lang.COMMENTS, + if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displaySmall, + textAlign: TextAlign.start, + ), + const Spacer(), + NamidaIconButton( + horizontalPadding: 0.0, + tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, + icon: Broken.refresh, + iconSize: 22.0, + onPressed: () async => await YoutubeController.inst.updateCurrentComments( + currentId, + forceRequest: ConnectivityController.inst.hasConnection, + ), + child: YoutubeController.inst.isCurrentCommentsFromCache + ? const StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + iconSize: 20.0, + secondaryIconSize: 12.0, + ) + : Icon( + Broken.refresh, + color: context.defaultIconColor(), + size: 20.0, + ), + ) + ], ), - if (channelIsVerified) ...[ - const SizedBox(width: 4.0), - const Icon( - Broken.shield_tick, - size: 12.0, - ), - ] + const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), + ShimmerWrapper( + shimmerEnabled: comments.isNotEmpty && comments.first == null, + child: YTCommentCardCompact(comment: comments.firstOrNull), + ) ], ), ), - const SizedBox(height: 2.0), - FittedBox( - child: NamidaDummyContainer( - width: 92.0, - height: 10.0, - borderRadius: 4.0, - shimmerEnabled: channelSubs == null, - child: Text( - [ - channelSubs?.formatDecimalShort(isTitleExpanded.value) ?? '?', - (channelSubs ?? 0) < 2 ? lang.SUBSCRIBER : lang.SUBSCRIBERS - ].join(' '), - style: context.textTheme.displaySmall?.copyWith( - fontSize: 12.0.multipliedFontScale, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 12.0), - Obx( - () { - final channelID = YoutubeSubscriptionsController.inst.idOrUrlToChannelID(channelIDOrURL); - final subscribed = YoutubeSubscriptionsController.inst.subscribedChannels[channelID]?.subscribed ?? false; - return TextButton( - style: TextButton.styleFrom( - foregroundColor: Color.alphaBlend(Colors.grey.withOpacity(subscribed ? 0.6 : 0.0), context.theme.colorScheme.primary), - ), - child: Row( - children: [ - Icon(subscribed ? Broken.tick_square : Broken.video, size: 20.0), - const SizedBox(width: 8.0), - Text( - subscribed ? lang.SUBSCRIBED : lang.SUBSCRIBE, - ), - ], - ), - onPressed: () async { - if (channelIDOrURL != null) { - await YoutubeSubscriptionsController.inst.changeChannelStatus(channelIDOrURL, null); - } - }, - ); - }, - ), - const SizedBox(width: 20.0), - ], - ), + ); + }, ), - ), - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - // --END-- channel + const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - // --SRART-- top comments - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - Obx( - () { - if (!settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; - final comments = YoutubeController.inst.currentComments; - return SliverToBoxAdapter( - child: comments.isEmpty - ? const SizedBox() - : NamidaInkWell( - key: Key("${currentId}_top_comments_highlight"), - bgColor: Color.alphaBlend(context.theme.scaffoldBackgroundColor.withOpacity(0.4), context.theme.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?.push( - GetPageRoute( - page: () => const YTMiniplayerCommentsSubpage(), - transition: Transition.cupertino, - ), + Obx( + () { + final feed = YoutubeController.inst.currentRelatedVideos; + if (feed.isNotEmpty && feed.first == null) { + return SliverToBoxAdapter( + key: Key("${currentId}_feed_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + key: Key("${currentId}_feedlist_shimmer"), + physics: const NeverScrollableScrollPhysics(), + itemCount: feed.length, + shrinkWrap: true, + itemBuilder: (context, index) { + const item = null; + return YoutubeVideoCard( + key: Key("${item == null}_${context.hashCode}"), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + isImageImportantInCache: false, + video: item, + playlistID: null, ); }, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon( - Broken.document, - size: 16.0, - ), - const SizedBox(width: 8.0), - Text( - [ - lang.COMMENTS, - if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), - ].join(' • '), - style: context.textTheme.displaySmall, - textAlign: TextAlign.start, - ), - const Spacer(), - NamidaIconButton( - horizontalPadding: 0.0, - tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async => await YoutubeController.inst.updateCurrentComments( - currentId, - forceRequest: ConnectivityController.inst.hasConnection, - ), - child: YoutubeController.inst.isCurrentCommentsFromCache - ? const StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - iconSize: 20.0, - secondaryIconSize: 12.0, - ) - : Icon( - Broken.refresh, - color: context.defaultIconColor(), - size: 20.0, - ), - ) - ], - ), - const NamidaContainerDivider(margin: EdgeInsets.symmetric(vertical: 4.0)), - ShimmerWrapper( - shimmerEnabled: comments.isNotEmpty && comments.first == null, - child: YTCommentCardCompact(comment: comments.firstOrNull), - ) - ], - ), ), - ); - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 4.0)), - - Obx( - () { - final feed = YoutubeController.inst.currentRelatedVideos; - if (feed.isNotEmpty && feed.first == null) { - return SliverToBoxAdapter( - key: Key("${currentId}_feed_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - key: Key("${currentId}_feedlist_shimmer"), - physics: const NeverScrollableScrollPhysics(), - itemCount: feed.length, - shrinkWrap: true, - itemBuilder: (context, index) { - const item = null; - return YoutubeVideoCard( - key: Key("${item == null}_${context.hashCode}"), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - isImageImportantInCache: false, - video: item, - playlistID: null, - ); - }, ), - ), + ); + } + return SliverFixedExtentList.builder( + key: Key("${currentId}_feedlist"), + itemExtent: relatedThumbnailItemExtent, + itemCount: feed.length, + itemBuilder: (context, index) { + final item = feed[index]; + if (item is StreamInfoItem || item == null) { + return YoutubeVideoCard( + key: Key("${item == null}_${context.hashCode}_${(item as StreamInfoItem?)?.id}"), + thumbnailHeight: relatedThumbnailHeight, + thumbnailWidth: relatedThumbnailWidth, + isImageImportantInCache: false, + video: item, + playlistID: null, + ); + } else if (item is YoutubePlaylist) { + return YoutubePlaylistCard( + key: Key("${context.hashCode}_${(item).id}"), + playlist: item, + playOnTap: true, + ); + } else if (item is YoutubeChannel) { + return YoutubeChannelCard( + key: Key("${context.hashCode}_${(item as YoutubeChannelCard).channel?.id}"), + channel: item, + ); + } + return const SizedBox(); + }, ); - } - return SliverFixedExtentList.builder( - key: Key("${currentId}_feedlist"), - itemExtent: relatedThumbnailItemExtent, - itemCount: feed.length, - itemBuilder: (context, index) { - final item = feed[index]; - if (item is StreamInfoItem || item == null) { - return YoutubeVideoCard( - key: Key("${item == null}_${context.hashCode}_${(item as StreamInfoItem?)?.id}"), - thumbnailHeight: relatedThumbnailHeight, - thumbnailWidth: relatedThumbnailWidth, - isImageImportantInCache: false, - video: item, - playlistID: null, - ); - } else if (item is YoutubePlaylist) { - return YoutubePlaylistCard( - key: Key("${context.hashCode}_${(item).id}"), - playlist: item, - playOnTap: true, - ); - } else if (item is YoutubeChannel) { - return YoutubeChannelCard( - key: Key("${context.hashCode}_${(item as YoutubeChannelCard).channel?.id}"), - channel: item, - ); - } - return const SizedBox(); - }, - ); - }, - ), - const SliverPadding(padding: EdgeInsets.only(top: 12.0)), + }, + ), + const SliverPadding(padding: EdgeInsets.only(top: 12.0)), - // --START-- Comments - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + // --START-- Comments + Obx( + () { + if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; - return SliverToBoxAdapter( - child: Padding( - key: Key("${currentId}_comments_header"), - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon(Broken.document), - const SizedBox(width: 8.0), - Text( - [ - lang.COMMENTS, - if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), - ].join(' • '), - style: context.textTheme.displayLarge, - textAlign: TextAlign.start, - ), - const Spacer(), - NamidaIconButton( - // key: Key(currentId), - tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, - icon: Broken.refresh, - iconSize: 22.0, - onPressed: () async => await YoutubeController.inst.updateCurrentComments( - currentId, - forceRequest: ConnectivityController.inst.hasConnection, - ), - child: YoutubeController.inst.isCurrentCommentsFromCache - ? const StackedIcon( - baseIcon: Broken.refresh, - secondaryIcon: Broken.global, - ) - : Icon( - Broken.refresh, - color: context.defaultIconColor(), - ), - ) - ], - ), - ), - ); - }, - ), - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - - final comments = YoutubeController.inst.currentComments; - if (comments.isNotEmpty && comments.first == null) { + final totalCommentsCount = YoutubeController.inst.currentTotalCommentsCount.value; return SliverToBoxAdapter( - key: Key("${currentId}_comments_shimmer"), - child: ShimmerWrapper( - transparent: false, - shimmerEnabled: true, - child: ListView.builder( - // key: Key(currentId), - physics: const NeverScrollableScrollPhysics(), - itemCount: comments.length, - shrinkWrap: true, - itemBuilder: (context, index) { - const comment = null; - return YTCommentCard( - key: Key("${comment == null}_${context.hashCode}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, - ); - }, + child: Padding( + key: Key("${currentId}_comments_header"), + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Broken.document), + const SizedBox(width: 8.0), + Text( + [ + lang.COMMENTS, + if (totalCommentsCount != null) totalCommentsCount.formatDecimalShort(), + ].join(' • '), + style: context.textTheme.displayLarge, + textAlign: TextAlign.start, + ), + const Spacer(), + NamidaIconButton( + // key: Key(currentId), + tooltip: YoutubeController.inst.isCurrentCommentsFromCache ? lang.CACHE : null, + icon: Broken.refresh, + iconSize: 22.0, + onPressed: () async => await YoutubeController.inst.updateCurrentComments( + currentId, + forceRequest: ConnectivityController.inst.hasConnection, + ), + child: YoutubeController.inst.isCurrentCommentsFromCache + ? const StackedIcon( + baseIcon: Broken.refresh, + secondaryIcon: Broken.global, + ) + : Icon( + Broken.refresh, + color: context.defaultIconColor(), + ), + ) + ], ), ), ); - } - return SliverList.builder( - key: Key("${currentId}_comments"), - itemCount: comments.length, - itemBuilder: (context, i) { - final comment = comments[i]; - return YTCommentCard( - key: Key("${comment == null}_${context.hashCode}_${comment?.commentId}"), - margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - comment: comment, - ); - }, - ); - }, - ), - Obx( - () { - if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + }, + ), + Obx( + () { + if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); - final isLoadingComments = YoutubeController.inst.isLoadingComments.value; - return isLoadingComments - ? SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: const Center( - child: LoadingIndicator(), - ).toSliver(), - ) - : const SizedBox().toSliver(); - }, - ), - ], - ), - () { - const containerHeight = 12.0; - return Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: YoutubeController.inst.shouldShowGlowUnderVideo - ? Stack( - key: const Key('actual_glow'), - children: [ - Container( - height: containerHeight, - color: context.theme.scaffoldBackgroundColor, + final comments = YoutubeController.inst.currentComments; + if (comments.isNotEmpty && comments.first == null) { + return SliverToBoxAdapter( + key: Key("${currentId}_comments_shimmer"), + child: ShimmerWrapper( + transparent: false, + shimmerEnabled: true, + child: ListView.builder( + // key: Key(currentId), + physics: const NeverScrollableScrollPhysics(), + itemCount: comments.length, + shrinkWrap: true, + itemBuilder: (context, index) { + const comment = null; + return YTCommentCard( + key: Key("${comment == null}_${context.hashCode}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ); + }, ), - Container( - height: containerHeight, - transform: Matrix4.translationValues(0, containerHeight / 2, 0), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: context.theme.scaffoldBackgroundColor, - spreadRadius: containerHeight * 0.25, - offset: const Offset(0, 0), - blurRadius: 8.0, - ), - ], + ), + ); + } + return SliverList.builder( + key: Key("${currentId}_comments"), + itemCount: comments.length, + itemBuilder: (context, i) { + final comment = comments[i]; + return YTCommentCard( + key: Key("${comment == null}_${context.hashCode}_${comment?.commentId}"), + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + comment: comment, + ); + }, + ); + }, + ), + Obx( + () { + if (settings.ytTopComments.value) return const SliverToBoxAdapter(child: SizedBox()); + + final isLoadingComments = YoutubeController.inst.isLoadingComments.value; + return isLoadingComments + ? SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: const Center( + child: LoadingIndicator(), + ).toSliver(), + ) + : const SizedBox().toSliver(); + }, + ), + ], + ), + () { + const containerHeight = 12.0; + return Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: YoutubeController.inst.shouldShowGlowUnderVideo + ? Stack( + key: const Key('actual_glow'), + children: [ + Container( + height: containerHeight, + color: context.theme.scaffoldBackgroundColor, ), - ), - ], - ) - : const SizedBox(key: Key('empty_glow')), - ), - ); - }() - ], + Container( + height: containerHeight, + transform: Matrix4.translationValues(0, containerHeight / 2, 0), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: context.theme.scaffoldBackgroundColor, + spreadRadius: containerHeight * 0.25, + offset: const Offset(0, 0), + blurRadius: 8.0, + ), + ], + ), + ), + ], + ) + : const SizedBox(key: Key('empty_glow')), + ), + ); + }() + ], + ), + false.obs, ), - false.obs, ), ), - ), - ], + ], + ), ), - ), - // -- dimming - Positioned.fill( - key: const Key('dimmie'), - child: IgnorePointer( - child: Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 600), - reverseDuration: const Duration(milliseconds: 200), - child: YoutubeController.inst.canDimMiniplayer - ? Container( - color: Colors.black.withOpacity(settings.ytMiniplayerDimOpacity.value), - ) - : null, + // -- dimming + Positioned.fill( + key: const Key('dimmie'), + child: IgnorePointer( + child: Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 600), + reverseDuration: const Duration(milliseconds: 200), + child: YoutubeController.inst.canDimMiniplayer + ? Container( + color: Colors.black.withOpacity(settings.ytMiniplayerDimOpacity.value), + ) + : null, + ), ), ), ), - ), - // prevent accidental scroll while performing home gesture - AbsorbPointer( - child: SizedBox( - height: 18.0, - width: context.width, + // prevent accidental scroll while performing home gesture + AbsorbPointer( + child: SizedBox( + height: 18.0, + width: context.width, + ), ), - ), - ], - ), - builder: (double height, double p, Widget? constantChild) { + ], + ), + // constant [1] + Column( + key: Key("${currentId}_title_button1_child"), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NamidaDummyContainer( + borderRadius: 4.0, + height: 16.0, + shimmerEnabled: videoInfo == null, + width: context.width - 24.0, + child: Text( + miniTitle ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 13.5.multipliedFontScale, + ), + ), + ), + const SizedBox(height: 4.0), + NamidaDummyContainer( + borderRadius: 4.0, + height: 10.0, + shimmerEnabled: videoInfo == null, + width: context.width - 24.0 * 2, + child: Text( + miniSubtitle ?? '', + style: context.textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 13.0.multipliedFontScale, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + + // constant [2] + Obx( + () { + final isLoading = Player.inst.shouldShowLoadingIndicator || VideoController.vcontroller.isBuffering; + 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: context.defaultIconColor(), + size: 36.0, + ), + ), + ), + NamidaIconButton( + horizontalPadding: 0.0, + onPressed: () { + Player.inst.togglePlayPause(); + }, + icon: null, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Player.inst.isPlaying + ? Icon( + Broken.pause, + color: context.defaultIconColor(), + key: const Key('pause'), + ) + : Icon( + Broken.play, + color: context.defaultIconColor(), + key: const Key('play'), + ), + ), + ), + ], + ); + }, + ), + // constant [3] + NamidaIconButton( + horizontalPadding: 0.0, + icon: Broken.next, + iconColor: context.defaultIconColor(), + onPressed: () { + Player.inst.next(); + }, + ), + ], + builder: (double height, double p, constantChildren) { 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; @@ -853,20 +950,20 @@ class YoutubeMiniPlayer extends StatelessWidget { Row( children: [ SizedBox(width: finalspace1sb), - Obx( - () { - final shouldShowVideo = VideoController.vcontroller.isInitialized; - return Container( - clipBehavior: Clip.antiAlias, - margin: EdgeInsets.symmetric(vertical: finalpadding), - decoration: BoxDecoration( - // color: shouldShowVideo ? Colors.black : CurrentColor.inst.color, - color: Colors.black, - borderRadius: BorderRadius.circular(finalbr), - ), - width: finalthumbnailWidth, - height: finalthumbnailHeight, - child: NamidaVideoWidget( + Container( + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.symmetric(vertical: finalpadding), + decoration: BoxDecoration( + // color: shouldShowVideo ? Colors.black : CurrentColor.inst.color, + color: Colors.black, + borderRadius: BorderRadius.circular(finalbr), + ), + width: finalthumbnailWidth, + height: finalthumbnailHeight, + child: Obx( + () { + final shouldShowVideo = VideoController.vcontroller.isInitialized; + return NamidaVideoWidget( key: Key("${currentId}_$shouldShowVideo"), enableControls: percentage > 0.5, onMinimizeTap: () { @@ -885,9 +982,9 @@ class YoutubeMiniPlayer extends StatelessWidget { compressed: false, preferLowerRes: false, ), - ), - ); - }, + ); + }, + ), ), if (reverseOpacity > 0) ...[ SizedBox(width: finalspace3sb), @@ -897,44 +994,7 @@ class YoutubeMiniPlayer extends StatelessWidget { key: Key("${currentId}_title_button1"), enabled: true, opacity: reverseOpacity, - child: Column( - key: Key("${currentId}_title_button1_child"), - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - NamidaDummyContainer( - borderRadius: 4.0, - height: 16.0, - shimmerEnabled: videoInfo == null, - width: context.width - 24.0, - child: Text( - miniTitle ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 13.5.multipliedFontScale, - ), - ), - ), - const SizedBox(height: 4.0), - NamidaDummyContainer( - borderRadius: 4.0, - height: 10.0, - shimmerEnabled: videoInfo == null, - width: context.width - 24.0 * 2, - child: Text( - miniSubtitle ?? '', - style: context.textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 13.0.multipliedFontScale, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + child: constantChildren[1], ), ), NamidaOpacity( @@ -945,51 +1005,7 @@ class YoutubeMiniPlayer extends StatelessWidget { key: Key("${currentId}_title_button2_child"), width: finalspace4buttons / 2, height: miniplayerHeight, - child: Obx( - () { - final isLoading = Player.inst.shouldShowLoadingIndicator || VideoController.vcontroller.isBuffering; - - 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: context.defaultIconColor(), - size: 36.0, - ), - ), - ), - NamidaIconButton( - horizontalPadding: 0.0, - onPressed: () { - Player.inst.togglePlayPause(); - }, - icon: null, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Player.inst.isPlaying - ? Icon( - Broken.pause, - color: context.defaultIconColor(), - key: const Key('pause'), - ) - : Icon( - Broken.play, - color: context.defaultIconColor(), - key: const Key('play'), - ), - ), - ), - ], - ); - }, - ), + child: constantChildren[2], ), ), NamidaOpacity( @@ -1000,14 +1016,7 @@ class YoutubeMiniPlayer extends StatelessWidget { key: Key("${currentId}_title_button3_child"), width: finalspace4buttons / 2, height: miniplayerHeight, - child: NamidaIconButton( - horizontalPadding: 0.0, - icon: Broken.next, - iconColor: context.defaultIconColor(), - onPressed: () { - Player.inst.next(); - }, - ), + child: constantChildren[3], ), ), SizedBox(width: finalspace5sb), @@ -1048,7 +1057,7 @@ class YoutubeMiniPlayer extends StatelessWidget { child: Stack( fit: StackFit.expand, children: [ - constantChild!, + constantChildren[0], IgnorePointer( child: ColoredBox( color: miniplayerBGColor.withOpacity(1 - percentageFast),