From 493800c28acf396a4ee557237e0fc3f648ade872 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Tue, 7 Nov 2023 04:27:59 +0200 Subject: [PATCH] feat: most played page for youtube --- android/app/src/main/AndroidManifest.xml | 3 - lib/controller/settings_controller.dart | 35 ++- lib/main.dart | 1 - .../pages/subpages/most_played_subpage.dart | 195 ++++++++++++++ .../subpages/playlist_tracks_subpage.dart | 237 ++++-------------- .../youtube_history_controller.dart | 6 +- lib/youtube/pages/youtube_home_view.dart | 6 +- lib/youtube/pages/yt_history_page.dart | 141 +---------- lib/youtube/pages/yt_playlist_subpage.dart | 62 +++++ .../widgets/yt_history_video_card.dart | 163 ++++++++++++ lib/youtube/yt_utils.dart | 3 +- 11 files changed, 514 insertions(+), 338 deletions(-) create mode 100644 lib/ui/pages/subpages/most_played_subpage.dart create mode 100644 lib/youtube/pages/yt_playlist_subpage.dart create mode 100644 lib/youtube/widgets/yt_history_video_card.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7993ff9f..1250fdb2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -95,9 +95,6 @@ - trackPlayMode = TrackPlayMode.searchResults.obs; - final Rx mostPlayedTimeRange = MostPlayedTimeRange.allTime.obs; + final mostPlayedTimeRange = MostPlayedTimeRange.allTime.obs; final mostPlayedCustomDateRange = DateRange.dummy().obs; final mostPlayedCustomisStartOfDay = true.obs; + final ytMostPlayedTimeRange = MostPlayedTimeRange.allTime.obs; + final ytMostPlayedCustomDateRange = DateRange.dummy().obs; + final ytMostPlayedCustomisStartOfDay = true.obs; + /// Track Items final RxBool displayThirdRow = true.obs; final RxBool displayThirdItemInEachRow = false.obs; @@ -441,6 +445,10 @@ class SettingsController { mostPlayedCustomDateRange.value = json['mostPlayedCustomDateRange'] != null ? DateRange.fromJson(json['mostPlayedCustomDateRange']) : mostPlayedCustomDateRange.value; mostPlayedCustomisStartOfDay.value = json['mostPlayedCustomisStartOfDay'] ?? mostPlayedCustomisStartOfDay.value; + ytMostPlayedTimeRange.value = MostPlayedTimeRange.values.getEnum(json['ytMostPlayedTimeRange']) ?? ytMostPlayedTimeRange.value; + ytMostPlayedCustomDateRange.value = json['ytMostPlayedCustomDateRange'] != null ? DateRange.fromJson(json['ytMostPlayedCustomDateRange']) : ytMostPlayedCustomDateRange.value; + ytMostPlayedCustomisStartOfDay.value = json['ytMostPlayedCustomisStartOfDay'] ?? ytMostPlayedCustomisStartOfDay.value; + /// Track Items displayThirdRow.value = json['displayThirdRow'] ?? displayThirdRow.value; displayThirdItemInEachRow.value = json['displayThirdItemInEachRow'] ?? displayThirdItemInEachRow.value; @@ -637,13 +645,16 @@ class SettingsController { 'localVideoMatchingCheckSameDir': localVideoMatchingCheckSameDir.value, 'playerRepeatMode': playerRepeatMode.value.convertToString, 'trackPlayMode': trackPlayMode.value.convertToString, - 'mostPlayedTimeRange': mostPlayedTimeRange.value.convertToString, 'onNotificationTapAction': onNotificationTapAction.value.convertToString, 'onYoutubeLinkOpen': onYoutubeLinkOpen.value.convertToString, 'performanceMode': performanceMode.value.convertToString, 'killPlayerAfterDismissingAppMode': killPlayerAfterDismissingAppMode.value.convertToString, + 'mostPlayedTimeRange': mostPlayedTimeRange.value.convertToString, 'mostPlayedCustomDateRange': mostPlayedCustomDateRange.value.toJson(), 'mostPlayedCustomisStartOfDay': mostPlayedCustomisStartOfDay.value, + 'ytMostPlayedTimeRange': ytMostPlayedTimeRange.value.convertToString, + 'ytMostPlayedCustomDateRange': ytMostPlayedCustomDateRange.value.toJson(), + 'ytMostPlayedCustomisStartOfDay': ytMostPlayedCustomisStartOfDay.value, /// Track Items 'displayThirdRow': displayThirdRow.value, @@ -813,13 +824,16 @@ class SettingsController { bool? localVideoMatchingCheckSameDir, RepeatMode? playerRepeatMode, TrackPlayMode? trackPlayMode, - MostPlayedTimeRange? mostPlayedTimeRange, NotificationTapAction? onNotificationTapAction, OnYoutubeLinkOpenAction? onYoutubeLinkOpen, PerformanceMode? performanceMode, KillAppMode? killPlayerAfterDismissingAppMode, + MostPlayedTimeRange? mostPlayedTimeRange, DateRange? mostPlayedCustomDateRange, bool? mostPlayedCustomisStartOfDay, + MostPlayedTimeRange? ytMostPlayedTimeRange, + DateRange? ytMostPlayedCustomDateRange, + bool? ytMostPlayedCustomisStartOfDay, bool? didSupportNamida, }) { if (selectedLanguage != null) { @@ -1276,9 +1290,6 @@ class SettingsController { if (trackPlayMode != null) { this.trackPlayMode.value = trackPlayMode; } - if (mostPlayedTimeRange != null) { - this.mostPlayedTimeRange.value = mostPlayedTimeRange; - } if (onNotificationTapAction != null) { this.onNotificationTapAction.value = onNotificationTapAction; } @@ -1291,12 +1302,24 @@ class SettingsController { if (killPlayerAfterDismissingAppMode != null) { this.killPlayerAfterDismissingAppMode.value = killPlayerAfterDismissingAppMode; } + if (mostPlayedTimeRange != null) { + this.mostPlayedTimeRange.value = mostPlayedTimeRange; + } if (mostPlayedCustomDateRange != null) { this.mostPlayedCustomDateRange.value = mostPlayedCustomDateRange; } if (mostPlayedCustomisStartOfDay != null) { this.mostPlayedCustomisStartOfDay.value = mostPlayedCustomisStartOfDay; } + if (ytMostPlayedTimeRange != null) { + this.ytMostPlayedTimeRange.value = ytMostPlayedTimeRange; + } + if (ytMostPlayedCustomDateRange != null) { + this.ytMostPlayedCustomDateRange.value = ytMostPlayedCustomDateRange; + } + if (ytMostPlayedCustomisStartOfDay != null) { + this.ytMostPlayedCustomisStartOfDay.value = ytMostPlayedCustomisStartOfDay; + } if (didSupportNamida != null) { this.didSupportNamida = didSupportNamida; diff --git a/lib/main.dart b/lib/main.dart index 017a0f35..c803ecea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:catcher/catcher.dart'; import 'package:external_path/external_path.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; diff --git a/lib/ui/pages/subpages/most_played_subpage.dart b/lib/ui/pages/subpages/most_played_subpage.dart new file mode 100644 index 00000000..7969daf8 --- /dev/null +++ b/lib/ui/pages/subpages/most_played_subpage.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:history_manager/history_manager.dart'; + +import 'package:namida/controller/current_color.dart'; +import 'package:namida/controller/navigator_controller.dart'; +import 'package:namida/core/dimensions.dart'; +import 'package:namida/core/extensions.dart'; +import 'package:namida/core/functions.dart'; +import 'package:namida/core/icon_fonts/broken_icons.dart'; +import 'package:namida/core/namida_converter_ext.dart'; +import 'package:namida/core/translations/language.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; + +class MostPlayedItemsPage extends StatelessWidget { + final HistoryManager historyController; + final bool Function(MostPlayedTimeRange type) isTimeRangeChipEnabled; + final void Function({required MostPlayedTimeRange? mptr, DateRange? dateCustom, bool? isStartOfDay}) onSavingTimeRange; + final List? itemExtents; + final Widget Function(Widget timeRangeChips, double bottomPadding) header; + final Widget Function(BuildContext context, int i, RxMap> listensMap) itemBuilder; + final Rx customDateRange; + + const MostPlayedItemsPage({ + super.key, + required this.historyController, + required this.isTimeRangeChipEnabled, + required this.onSavingTimeRange, + required this.itemExtents, + required this.header, + required this.itemBuilder, + required this.customDateRange, + }); + + void _onSelectingTimeRange({ + required MostPlayedTimeRange? mptr, + DateRange? dateCustom, + bool? isStartOfDay, + }) { + onSavingTimeRange(mptr: mptr, dateCustom: dateCustom, isStartOfDay: isStartOfDay); + + historyController.updateTempMostPlayedPlaylist( + mptr: mptr, + customDateRange: dateCustom, + isStartOfDay: isStartOfDay, + ); + NamidaNavigator.inst.closeDialog(); + } + + bool _isEnabled(MostPlayedTimeRange type) => isTimeRangeChipEnabled(type); + + Widget _getChipChild({ + required BuildContext context, + DateRange? dateCustom, + required MostPlayedTimeRange mptr, + Widget? Function(Color? textColor)? trailing, + }) { + final dateText = dateCustom == null || dateCustom == DateRange.dummy() + ? null + : "${dateCustom.oldest.millisecondsSinceEpoch.dateFormattedOriginalNoYears(dateCustom.newest)} → ${dateCustom.newest.millisecondsSinceEpoch.dateFormattedOriginalNoYears(dateCustom.oldest)}"; + + final textColor = _isEnabled(mptr) ? const Color.fromARGB(200, 255, 255, 255) : null; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: GestureDetector( + onTap: () => _onSelectingTimeRange( + dateCustom: dateCustom, + mptr: mptr, + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + decoration: BoxDecoration( + color: _isEnabled(mptr) ? CurrentColor.inst.currentColorScheme.withAlpha(160) : context.theme.cardColor, + borderRadius: BorderRadius.circular(8.0.multipliedRadius), + ), + child: Row( + children: [ + Text( + dateText ?? mptr.toText(), + style: context.textTheme.displaySmall?.copyWith( + color: textColor, + fontSize: dateText == null ? null : 12.0.multipliedFontScale, + fontWeight: FontWeight.w600, + ), + ), + if (trailing != null) ...[ + const SizedBox(width: 2.0), + trailing(textColor)!, + ] + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return BackgroundWrapper( + child: Obx( + () { + final finalListenMap = historyController.currentTopTracksMapListens; + final mostplayedOptions = List.from(MostPlayedTimeRange.values)..remove(MostPlayedTimeRange.custom); + final bottomWidget = Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const SizedBox(width: 8.0), + NamidaInkWell( + animationDurationMS: 200, + borderRadius: 6.0, + bgColor: context.theme.cardTheme.color, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + border: _isEnabled(MostPlayedTimeRange.custom) ? Border.all(color: CurrentColor.inst.color) : null, + borderRadius: BorderRadius.circular(8.0.multipliedRadius), + ), + child: Row( + children: [ + const Icon(Broken.calendar, size: 18.0), + const SizedBox(width: 4.0), + Text( + lang.CUSTOM, + style: context.textTheme.displayMedium, + ), + const SizedBox(width: 4.0), + const Icon(Broken.arrow_down_2, size: 14.0), + ], + ), + onTap: () { + showCalendarDialog( + title: lang.CHOOSE, + buttonText: lang.CONFIRM, + useHistoryDates: true, + onGenerate: (dates) => _onSelectingTimeRange( + dateCustom: DateRange(oldest: dates.first, newest: dates.last), + mptr: MostPlayedTimeRange.custom, + ), + ); + }, + ), + const SizedBox(width: 4.0), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Obx( + () { + final dateRange = customDateRange.value; + return _getChipChild( + context: context, + mptr: MostPlayedTimeRange.custom, + dateCustom: dateRange, + trailing: (textColor) => NamidaIconButton( + padding: EdgeInsets.zero, + icon: Broken.close_circle, + iconSize: 14.0, + iconColor: textColor, + onPressed: () => _onSelectingTimeRange(mptr: MostPlayedTimeRange.allTime, dateCustom: DateRange.dummy()), + ), + ).animateEntrance( + showWhen: dateRange.oldest != DateTime(0), + durationMS: 400, + reverseDurationMS: 200, + ); + }, + ), + ...mostplayedOptions.map( + (action) => _getChipChild( + context: context, + mptr: action, + ), + ), + ], + ), + ), + ), + ], + ), + ); + const bottomPadding = 0.0; + return NamidaListView( + itemExtents: itemExtents, + header: header(bottomWidget, bottomPadding), + padding: const EdgeInsets.only(bottom: kBottomPadding), + itemCount: finalListenMap.length, + itemBuilder: (context, i) => itemBuilder(context, i, finalListenMap), + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/subpages/playlist_tracks_subpage.dart b/lib/ui/pages/subpages/playlist_tracks_subpage.dart index 4f5b0200..c8906aab 100644 --- a/lib/ui/pages/subpages/playlist_tracks_subpage.dart +++ b/lib/ui/pages/subpages/playlist_tracks_subpage.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:history_manager/history_manager.dart'; import 'package:known_extents_list_view_builder/sliver_known_extents_list.dart'; import 'package:sticky_headers/sticky_headers.dart'; import 'package:namida/class/track.dart'; import 'package:namida/controller/current_color.dart'; import 'package:namida/controller/history_controller.dart'; -import 'package:namida/controller/navigator_controller.dart'; import 'package:namida/controller/playlist_controller.dart'; import 'package:namida/controller/settings_controller.dart'; import 'package:namida/core/constants.dart'; @@ -22,6 +20,7 @@ import 'package:namida/core/translations/language.dart'; import 'package:namida/ui/dialogs/common_dialogs.dart'; import 'package:namida/ui/dialogs/general_popup_dialog.dart'; import 'package:namida/ui/dialogs/track_listens_dialog.dart'; +import 'package:namida/ui/pages/subpages/most_played_subpage.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/library/multi_artwork_container.dart'; import 'package:namida/ui/widgets/library/track_tile.dart'; @@ -171,83 +170,25 @@ class HistoryTracksPage extends StatelessWidget { class MostPlayedTracksPage extends StatelessWidget { const MostPlayedTracksPage({super.key}); - void _onSelectingTimeRange({ - required MostPlayedTimeRange? mptr, - DateRange? dateCustom, - bool? isStartOfDay, - }) { - settings.save( - mostPlayedTimeRange: mptr, - mostPlayedCustomDateRange: dateCustom, - mostPlayedCustomisStartOfDay: isStartOfDay, - ); - HistoryController.inst.updateTempMostPlayedPlaylist( - mptr: mptr, - customDateRange: dateCustom, - isStartOfDay: isStartOfDay, - ); - NamidaNavigator.inst.closeDialog(); - } - - bool _isEnabled(MostPlayedTimeRange type) => type == settings.mostPlayedTimeRange.value; - - Widget _getChipChild({ - required BuildContext context, - DateRange? dateCustom, - required MostPlayedTimeRange mptr, - Widget? Function(Color? textColor)? trailing, - }) { - final dateText = dateCustom == null || dateCustom == DateRange.dummy() - ? null - : "${dateCustom.oldest.millisecondsSinceEpoch.dateFormattedOriginalNoYears(dateCustom.newest)} → ${dateCustom.newest.millisecondsSinceEpoch.dateFormattedOriginalNoYears(dateCustom.oldest)}"; - - final textColor = _isEnabled(mptr) ? const Color.fromARGB(200, 255, 255, 255) : null; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0), - child: GestureDetector( - onTap: () => _onSelectingTimeRange( - dateCustom: dateCustom, - mptr: mptr, - ), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - decoration: BoxDecoration( - color: _isEnabled(mptr) ? CurrentColor.inst.currentColorScheme.withAlpha(160) : context.theme.cardColor, - borderRadius: BorderRadius.circular(8.0.multipliedRadius), - ), - child: Row( - children: [ - Text( - dateText ?? mptr.toText(), - style: context.textTheme.displaySmall?.copyWith( - color: textColor, - fontSize: dateText == null ? null : 12.0.multipliedFontScale, - fontWeight: FontWeight.w600, - ), - ), - if (trailing != null) ...[ - const SizedBox(width: 2.0), - trailing(textColor)!, - ] - ], - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { - return BackgroundWrapper( - child: Obx( - () { - final finalListenMap = HistoryController.inst.currentTopTracksMapListens; - final tracks = QueueSource.mostPlayed.toTracks(); - final mostplayedOptions = List.from(MostPlayedTimeRange.values)..remove(MostPlayedTimeRange.custom); - return NamidaListView( - itemExtents: tracks.toTrackItemExtents(), - header: SubpagesTopContainer( + return Obx( + () { + final tracks = QueueSource.mostPlayed.toTracks(); + return MostPlayedItemsPage( + itemExtents: tracks.toTrackItemExtents(), + historyController: HistoryController.inst, + customDateRange: settings.mostPlayedCustomDateRange, + isTimeRangeChipEnabled: (type) => type == settings.mostPlayedTimeRange.value, + onSavingTimeRange: ({dateCustom, isStartOfDay, mptr}) { + settings.save( + mostPlayedTimeRange: mptr, + mostPlayedCustomDateRange: dateCustom, + mostPlayedCustomisStartOfDay: isStartOfDay, + ); + }, + header: (timeRangeChips, bottomPadding) { + return SubpagesTopContainer( source: QueueSource.mostPlayed, title: k_PLAYLIST_NAME_MOST_PLAYED.translatePlaylistName(), subtitle: tracks.displayTrackKeyword, @@ -258,119 +199,41 @@ class MostPlayedTracksPage extends StatelessWidget { paths: tracks.toImagePaths(), ), tracks: tracks, - bottomPadding: 0.0, - bottomWidget: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const SizedBox(width: 8.0), - NamidaInkWell( - animationDurationMS: 200, - borderRadius: 6.0, - bgColor: context.theme.cardTheme.color, - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: _isEnabled(MostPlayedTimeRange.custom) ? Border.all(color: CurrentColor.inst.color) : null, - borderRadius: BorderRadius.circular(8.0.multipliedRadius), - ), - child: Row( - children: [ - const Icon(Broken.calendar, size: 18.0), - const SizedBox(width: 4.0), - Text( - 'Custom', - style: context.textTheme.displayMedium, - ), - const SizedBox(width: 4.0), - const Icon(Broken.arrow_down_2, size: 14.0), - ], - ), - onTap: () { - showCalendarDialog( - title: lang.CHOOSE, - buttonText: lang.CONFIRM, - useHistoryDates: true, - onGenerate: (dates) => _onSelectingTimeRange( - dateCustom: DateRange(oldest: dates.first, newest: dates.last), - mptr: MostPlayedTimeRange.custom, - ), - ); - }, - ), - const SizedBox(width: 4.0), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - Obx( - () { - final dateRange = settings.mostPlayedCustomDateRange.value; - return _getChipChild( - context: context, - mptr: MostPlayedTimeRange.custom, - dateCustom: dateRange, - trailing: (textColor) => NamidaIconButton( - padding: EdgeInsets.zero, - icon: Broken.close_circle, - iconSize: 14.0, - iconColor: textColor, - onPressed: () => _onSelectingTimeRange(mptr: MostPlayedTimeRange.allTime, dateCustom: DateRange.dummy()), - ), - ).animateEntrance( - showWhen: dateRange.oldest != DateTime(0), - durationMS: 400, - reverseDurationMS: 200, - ); - }, - ), - ...mostplayedOptions.map( - (action) => _getChipChild( - context: context, - mptr: action, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - padding: const EdgeInsets.only(bottom: kBottomPadding), - itemCount: finalListenMap.length, - itemBuilder: (context, i) { - final track = tracks[i]; - final listens = finalListenMap[track] ?? []; + bottomPadding: bottomPadding, + bottomWidget: timeRangeChips, + ); + }, + itemBuilder: (context, i, listensMap) { + final track = tracks[i]; + final listens = listensMap[track] ?? []; - return AnimatingTile( - key: ValueKey(i), - position: i, - child: TrackTile( - draggableThumbnail: false, - index: i, - trackOrTwd: tracks[i], - queueSource: QueueSource.mostPlayed, - playlistName: k_PLAYLIST_NAME_MOST_PLAYED, - onRightAreaTap: () => showTrackListensDialog(track.track, datesOfListen: listens), - trailingWidget: Container( - padding: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - color: context.theme.scaffoldBackgroundColor, - shape: BoxShape.circle, - ), - child: Text( - listens.length.formatDecimal(), - style: context.textTheme.displaySmall, - ), + return AnimatingTile( + key: Key("${track}_$i"), + position: i, + child: TrackTile( + key: Key("${track}_$i"), + draggableThumbnail: false, + index: i, + trackOrTwd: tracks[i], + queueSource: QueueSource.mostPlayed, + playlistName: k_PLAYLIST_NAME_MOST_PLAYED, + onRightAreaTap: () => showTrackListensDialog(track.track, datesOfListen: listens), + trailingWidget: Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: context.theme.scaffoldBackgroundColor, + shape: BoxShape.circle, + ), + child: Text( + listens.length.formatDecimal(), + style: context.textTheme.displaySmall, ), ), - ); - }, - ); - }, - ), + ), + ); + }, + ); + }, ); } } diff --git a/lib/youtube/controller/youtube_history_controller.dart b/lib/youtube/controller/youtube_history_controller.dart index df66c31e..9dffed16 100644 --- a/lib/youtube/controller/youtube_history_controller.dart +++ b/lib/youtube/controller/youtube_history_controller.dart @@ -62,13 +62,13 @@ class YoutubeHistoryController with HistoryManager { } @override - MostPlayedTimeRange get currentMostPlayedTimeRange => settings.mostPlayedTimeRange.value; + MostPlayedTimeRange get currentMostPlayedTimeRange => settings.ytMostPlayedTimeRange.value; @override - DateRange get mostPlayedCustomDateRange => settings.mostPlayedCustomDateRange.value; + DateRange get mostPlayedCustomDateRange => settings.ytMostPlayedCustomDateRange.value; @override - bool get mostPlayedCustomIsStartOfDay => settings.mostPlayedCustomisStartOfDay.value; + bool get mostPlayedCustomIsStartOfDay => settings.ytMostPlayedCustomisStartOfDay.value; @override double get trackTileItemExtent => Dimensions.youtubeCardItemExtent; diff --git a/lib/youtube/pages/youtube_home_view.dart b/lib/youtube/pages/youtube_home_view.dart index 2fb52e37..4aa0cbbb 100644 --- a/lib/youtube/pages/youtube_home_view.dart +++ b/lib/youtube/pages/youtube_home_view.dart @@ -4,14 +4,15 @@ import 'package:namida/core/translations/language.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/youtube/pages/youtube_page.dart'; import 'package:namida/youtube/pages/yt_history_page.dart'; +import 'package:namida/youtube/pages/yt_playlist_subpage.dart'; class YouTubeHomeView extends StatelessWidget { const YouTubeHomeView({super.key}); @override Widget build(BuildContext context) { - final tabs = [lang.HOME, lang.HISTORY]; - final historyIndex = tabs.length - 1; + final tabs = [lang.HOME, lang.HISTORY, lang.MOST_PLAYED]; + const historyIndex = 1; return BackgroundWrapper( child: NamidaTabView( initialIndex: historyIndex, @@ -20,6 +21,7 @@ class YouTubeHomeView extends StatelessWidget { children: const [ YoutubePage(), YoutubeHistoryPage(), + MostPlayedYTVideosPage(), ], ), ); diff --git a/lib/youtube/pages/yt_history_page.dart b/lib/youtube/pages/yt_history_page.dart index 952b1a30..00371f98 100644 --- a/lib/youtube/pages/yt_history_page.dart +++ b/lib/youtube/pages/yt_history_page.dart @@ -1,22 +1,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:known_extents_list_view_builder/sliver_known_extents_list.dart'; -import 'package:playlist_manager/module/playlist_id.dart'; import 'package:sticky_headers/sticky_headers.dart'; import 'package:namida/controller/current_color.dart'; -import 'package:namida/controller/player_controller.dart'; -import 'package:namida/controller/settings_controller.dart'; -import 'package:namida/core/constants.dart'; import 'package:namida/core/dimensions.dart'; -import 'package:namida/core/enums.dart'; import 'package:namida/core/extensions.dart'; import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; -import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; -import 'package:namida/youtube/widgets/yt_thumbnail.dart'; -import 'package:namida/youtube/yt_utils.dart'; +import 'package:namida/youtube/widgets/yt_history_video_card.dart'; class YoutubeHistoryPage extends StatelessWidget { const YoutubeHistoryPage({super.key}); @@ -34,6 +27,7 @@ class YoutubeHistoryPage extends StatelessWidget { key: UniqueKey(), itemExtents: YoutubeHistoryController.inst.allItemsExtentsHistory, delegate: SliverChildBuilderDelegate( + childCount: YoutubeHistoryController.inst.historyDays.length, (context, index) { final day = days[index]; final dayInMs = Duration(days: day).inMilliseconds; @@ -95,132 +89,10 @@ class YoutubeHistoryPage extends StatelessWidget { itemExtent: Dimensions.youtubeCardItemExtent, itemCount: videos.length, itemBuilder: (context, i) { - final video = videos[i]; - - final info = YoutubeController.inst.fetchVideoDetailsFromCacheSync(video.id); - final duration = info?.duration?.inSeconds.secondsLabel; - final menuItems = YTUtils.getVideoCardMenuItems( - videoId: video.id, - url: info?.url, - playlistID: const PlaylistID(id: k_PLAYLIST_NAME_HISTORY), - idsNamesLookup: {video.id: info?.name}, - ); - final backupVideoInfo = YoutubeController.inst.getBackupVideoInfo(video.id); - final videoTitle = info?.name ?? backupVideoInfo?.title ?? video.id; - final videoSubtitle = info?.uploaderName ?? backupVideoInfo?.channel; - final dateText = video.dateTimeAdded.millisecondsSinceEpoch.dateAndClockFormattedOriginal; - - return Obx( - () { - final isCurrentlyPlaying = Player.inst.nowPlayingVideoID == video; - final hightlightedColor = day == YoutubeHistoryController.inst.dayOfHighLight.value && i == YoutubeHistoryController.inst.indexToHighlight.value - ? context.theme.colorScheme.onBackground.withAlpha(40) - : null; - return NamidaPopupWrapper( - openOnTap: false, - childrenDefault: menuItems, - child: NamidaInkWell( - onTap: () { - YTUtils.expandMiniplayer(); - Player.inst.playOrPause(i, videos, QueueSource.others); - }, - height: Dimensions.youtubeCardItemExtent, - margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: Dimensions.youtubeCardItemVerticalPadding), - bgColor: isCurrentlyPlaying ? CurrentColor.inst.color.withAlpha(140) : (hightlightedColor ?? context.theme.cardColor), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0.multipliedRadius), - ), - child: Stack( - children: [ - Row( - children: [ - const SizedBox(width: Dimensions.youtubeCardItemVerticalPadding), - YoutubeThumbnail( - key: Key(video.id), - isImportantInCache: true, - width: (Dimensions.youtubeCardItemHeight * 16 / 9) - 3.0, - height: Dimensions.youtubeCardItemHeight - 3.0, - videoId: video.id, - onTopWidgets: [ - if (duration != null) - Positioned( - bottom: 0.0, - right: 0.0, - child: Padding( - padding: const EdgeInsets.all(3.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(6.0.multipliedRadius), - child: NamidaBgBlur( - blur: 2.0, - enabled: settings.enableBlurEffect.value, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 1.0), - color: Colors.black.withOpacity(0.2), - child: Text( - duration, - style: context.textTheme.displaySmall?.copyWith( - color: Colors.white.withOpacity(0.8), - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(width: 12.0), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - videoTitle, - maxLines: 2, - style: context.textTheme.displayMedium?.copyWith( - color: isCurrentlyPlaying ? Colors.white.withOpacity(0.7) : null, - ), - ), - if (videoSubtitle != null) - Text( - videoSubtitle, - maxLines: 1, - style: context.textTheme.displaySmall?.copyWith( - color: isCurrentlyPlaying ? Colors.white.withOpacity(0.6) : null, - ), - ), - Text( - dateText, - maxLines: 1, - style: context.textTheme.displaySmall?.copyWith( - color: isCurrentlyPlaying ? Colors.white.withOpacity(0.5) : null, - ), - ), - ], - ), - ), - const SizedBox(width: 12.0), - ], - ), - Positioned( - bottom: 6.0, - right: 12.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: YTUtils.getVideoCacheStatusIcons( - context: context, - videoId: video.id, - iconsColor: isCurrentlyPlaying ? Colors.white.withOpacity(0.5) : null, - ), - ), - ), - ], - ), - ), - ); - }, + return YTHistoryVideoCard( + videos: videos, + index: i, + day: day, ); }, ), @@ -228,7 +100,6 @@ class YoutubeHistoryPage extends StatelessWidget { ), ); }, - childCount: YoutubeHistoryController.inst.historyDays.length, ), ); }, diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart new file mode 100644 index 00000000..f13698b6 --- /dev/null +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:playlist_manager/module/playlist_id.dart'; + +import 'package:namida/controller/settings_controller.dart'; +import 'package:namida/core/constants.dart'; +import 'package:namida/core/dimensions.dart'; +import 'package:namida/ui/pages/subpages/most_played_subpage.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; +import 'package:namida/youtube/controller/youtube_history_controller.dart'; +import 'package:namida/youtube/widgets/yt_history_video_card.dart'; + +class MostPlayedYTVideosPage extends StatelessWidget { + const MostPlayedYTVideosPage({super.key}); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final videos = YoutubeHistoryController.inst.currentMostPlayedTracks.toList(); + return MostPlayedItemsPage( + itemExtents: List.filled(videos.length, Dimensions.youtubeCardItemExtent), + historyController: YoutubeHistoryController.inst, + customDateRange: settings.ytMostPlayedCustomDateRange, + isTimeRangeChipEnabled: (type) => type == settings.ytMostPlayedTimeRange.value, + onSavingTimeRange: ({dateCustom, isStartOfDay, mptr}) { + settings.save( + ytMostPlayedTimeRange: mptr, + ytMostPlayedCustomDateRange: dateCustom, + ytMostPlayedCustomisStartOfDay: isStartOfDay, + ); + }, + header: (timeRangeChips, bottomPadding) { + return Padding( + padding: const EdgeInsets.only(top: 4.0), + child: timeRangeChips, + ); + }, + itemBuilder: (context, i, listensMap) { + final videoID = videos[i]; + final listens = listensMap[videoID] ?? []; + + return YTHistoryVideoCard( + key: Key("${videoID}_$i"), + videos: videos + .map( + (e) => YoutubeID( + id: e, + playlistID: const PlaylistID(id: k_PLAYLIST_NAME_MOST_PLAYED), + ), + ) + .toList(), + index: i, + day: null, + overrideListens: listens, + ); + }, + ); + }, + ); + } +} diff --git a/lib/youtube/widgets/yt_history_video_card.dart b/lib/youtube/widgets/yt_history_video_card.dart new file mode 100644 index 00000000..0cc3a568 --- /dev/null +++ b/lib/youtube/widgets/yt_history_video_card.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:playlist_manager/module/playlist_id.dart'; + +import 'package:namida/controller/current_color.dart'; +import 'package:namida/controller/player_controller.dart'; +import 'package:namida/controller/settings_controller.dart'; +import 'package:namida/core/constants.dart'; +import 'package:namida/core/dimensions.dart'; +import 'package:namida/core/enums.dart'; +import 'package:namida/core/extensions.dart'; +import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/youtube_id.dart'; +import 'package:namida/youtube/controller/youtube_controller.dart'; +import 'package:namida/youtube/controller/youtube_history_controller.dart'; +import 'package:namida/youtube/widgets/yt_thumbnail.dart'; +import 'package:namida/youtube/yt_utils.dart'; + +class YTHistoryVideoCard extends StatelessWidget { + final List videos; + final int? day; + final int index; + final List overrideListens; + + const YTHistoryVideoCard({ + super.key, + required this.videos, + required this.day, + required this.index, + this.overrideListens = const [], + }); + + @override + Widget build(BuildContext context) { + final video = videos[index]; + final info = YoutubeController.inst.fetchVideoDetailsFromCacheSync(video.id); + final duration = info?.duration?.inSeconds.secondsLabel; + final menuItems = YTUtils.getVideoCardMenuItems( + videoId: video.id, + url: info?.url, + playlistID: const PlaylistID(id: k_PLAYLIST_NAME_HISTORY), + idsNamesLookup: {video.id: info?.name}, + ); + final backupVideoInfo = YoutubeController.inst.getBackupVideoInfo(video.id); + final videoTitle = info?.name ?? backupVideoInfo?.title ?? video.id; + final videoSubtitle = info?.uploaderName ?? backupVideoInfo?.channel; + final dateText = video.dateTimeAdded.millisecondsSinceEpoch.dateAndClockFormattedOriginal; + + return Obx( + () { + final isCurrentlyPlaying = Player.inst.nowPlayingVideoID == video; + final sameDay = day == YoutubeHistoryController.inst.dayOfHighLight.value; + final sameIndex = index == YoutubeHistoryController.inst.indexToHighlight.value; + final hightlightedColor = sameDay && sameIndex ? context.theme.colorScheme.onBackground.withAlpha(40) : null; + return NamidaPopupWrapper( + openOnTap: false, + childrenDefault: menuItems, + child: NamidaInkWell( + onTap: () { + YTUtils.expandMiniplayer(); + Player.inst.playOrPause(index, videos, QueueSource.others); + }, + height: Dimensions.youtubeCardItemExtent, + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: Dimensions.youtubeCardItemVerticalPadding), + bgColor: isCurrentlyPlaying ? CurrentColor.inst.color.withAlpha(140) : (hightlightedColor ?? context.theme.cardColor), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0.multipliedRadius), + ), + child: Stack( + children: [ + Row( + children: [ + const SizedBox(width: Dimensions.youtubeCardItemVerticalPadding), + YoutubeThumbnail( + key: Key(video.id), + isImportantInCache: true, + width: (Dimensions.youtubeCardItemHeight * 16 / 9) - 3.0, + height: Dimensions.youtubeCardItemHeight - 3.0, + videoId: video.id, + onTopWidgets: [ + if (duration != null) + Positioned( + bottom: 0.0, + right: 0.0, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.0.multipliedRadius), + child: NamidaBgBlur( + blur: 2.0, + enabled: settings.enableBlurEffect.value, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 1.0), + color: Colors.black.withOpacity(0.2), + child: Text( + duration, + style: context.textTheme.displaySmall?.copyWith( + color: Colors.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(width: 12.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoTitle, + maxLines: 2, + style: context.textTheme.displayMedium?.copyWith( + color: isCurrentlyPlaying ? Colors.white.withOpacity(0.7) : null, + ), + ), + if (videoSubtitle != null) + Text( + videoSubtitle, + maxLines: 1, + style: context.textTheme.displaySmall?.copyWith( + color: isCurrentlyPlaying ? Colors.white.withOpacity(0.6) : null, + ), + ), + Text( + dateText, + maxLines: 1, + style: context.textTheme.displaySmall?.copyWith( + color: isCurrentlyPlaying ? Colors.white.withOpacity(0.5) : null, + ), + ), + ], + ), + ), + const SizedBox(width: 12.0), + ], + ), + Positioned( + bottom: 6.0, + right: 12.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: YTUtils.getVideoCacheStatusIcons( + context: context, + videoId: video.id, + iconsColor: isCurrentlyPlaying ? Colors.white.withOpacity(0.5) : null, + overrideListens: overrideListens, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/youtube/yt_utils.dart b/lib/youtube/yt_utils.dart index b5d2030b..6e2dc03d 100644 --- a/lib/youtube/yt_utils.dart +++ b/lib/youtube/yt_utils.dart @@ -46,8 +46,9 @@ class YTUtils { required String videoId, required BuildContext context, Color? iconsColor, + List overrideListens = const [], }) { - final listens = YoutubeHistoryController.inst.topTracksMapListens[videoId] ?? []; + final listens = overrideListens.isNotEmpty ? overrideListens : YoutubeHistoryController.inst.topTracksMapListens[videoId] ?? []; return [ if (listens.isNotEmpty) NamidaInkWell(