diff --git a/lib/app/view/scaffold.dart b/lib/app/view/scaffold.dart index 3b3ca0ffa..ea798fcf9 100644 --- a/lib/app/view/scaffold.dart +++ b/lib/app/view/scaffold.dart @@ -74,19 +74,21 @@ class _MusicPodScaffoldState extends State { children: [ const Expanded(child: MasterDetailPage()), if (!playerToTheRight || isMobile) - const PlayerView(mode: PlayerPosition.bottom), + const PlayerView(position: PlayerPosition.bottom), ], ), ), if (playerToTheRight) const SizedBox( width: kSideBarPlayerWidth, - child: PlayerView(mode: PlayerPosition.sideBar), + child: PlayerView(position: PlayerPosition.sideBar), ), ], ), if (isFullScreen == true) - const Scaffold(body: PlayerView(mode: PlayerPosition.fullWindow)), + const Scaffold( + body: PlayerView(position: PlayerPosition.fullWindow), + ), ], ); } diff --git a/lib/common/view/snackbars.dart b/lib/common/view/snackbars.dart index 425716610..c59f9ca50 100644 --- a/lib/common/view/snackbars.dart +++ b/lib/common/view/snackbars.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../l10n/l10n.dart'; -import '../../radio/radio_model.dart'; ScaffoldFeatureController showSnackBar({ required BuildContext context, Widget? content, SnackBar? snackBar, Duration? duration, - clear = true, + bool clear = true, }) { if (clear) { ScaffoldMessenger.of(context).clearSnackBars(); @@ -22,36 +18,3 @@ ScaffoldFeatureController showSnackBar({ ), ); } - -SnackBar buildConnectSnackBar({ - required String? connectedHost, - required BuildContext context, -}) { - return SnackBar( - duration: connectedHost != null - ? const Duration(seconds: 1) - : const Duration(seconds: 30), - content: Text( - connectedHost != null - ? '${context.l10n.connectedTo}: $connectedHost' - : context.l10n.noRadioServerFound, - ), - action: (connectedHost == null) - ? SnackBarAction( - onPressed: () async { - final connectedHost = await di().init(); - if (context.mounted) { - showSnackBar( - context: context, - content: buildConnectSnackBar( - connectedHost: connectedHost, - context: context, - ), - ); - } - }, - label: context.l10n.tryReconnect, - ) - : null, - ); -} diff --git a/lib/player/player_mixin.dart b/lib/player/player_mixin.dart deleted file mode 100644 index 38a439da1..000000000 --- a/lib/player/player_mixin.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../app/app_model.dart'; -import '../common/data/audio.dart'; -import '../common/view/copy_clipboard_content.dart'; -import '../common/view/snackbars.dart'; -import '../library/library_model.dart'; -import '../local_audio/local_audio_model.dart'; -import '../local_audio/view/album_page.dart'; -import '../local_audio/view/artist_page.dart'; -import '../podcasts/podcast_utils.dart'; -import '../radio/view/station_page.dart'; - -mixin PlayerMixin { - String getSubTitle(Audio? audio) => (switch (audio?.audioType) { - AudioType.podcast => audio?.album, - AudioType.radio => audio?.title, - _ => audio?.artist - } ?? - '') - .trim(); - - String getTitle({required Audio? audio, required String? icyTitle}) => - icyTitle?.isNotEmpty == true && audio?.audioType == AudioType.radio - ? icyTitle! - : (audio?.title?.isNotEmpty == true ? audio!.title! : '').trim(); - - void onTitleTap({ - Audio? audio, - String? text, - required BuildContext context, - }) { - if (text?.isNotEmpty == true && audio?.audioType == AudioType.radio || - audio?.audioType == null) { - showSnackBar( - context: context, - content: CopyClipboardContent(text: text!), - ); - return; - } - - switch (audio?.audioType) { - case AudioType.local: - _onLocalAudioTitleTap( - audio: audio!, - ); - return; - case AudioType.radio: - case AudioType.podcast: - if (audio?.url == null) return; - showSnackBar( - context: context, - content: CopyClipboardContent( - text: audio!.url!, - onSearch: () => launchUrl(Uri.parse(audio.url!)), - ), - ); - return; - default: - return; - } - } - - void _onLocalAudioTitleTap({required Audio audio}) { - if (audio.album == null) return; - final localAudioModel = di(); - localAudioModel.init().then( - (value) { - final albumAudios = localAudioModel.findAlbum(audio.album!); - if (albumAudios?.firstOrNull == null) return; - final id = albumAudios!.first.albumId; - if (id == null) return; - - di().setFullWindowMode(false); - - final libraryModel = di(); - libraryModel.push( - builder: (_) => AlbumPage( - id: id, - album: albumAudios, - ), - pageId: id, - ); - }, - ); - } - - void onArtistTap({required Audio audio, required BuildContext context}) { - switch (audio.audioType) { - case AudioType.local: - _onLocalAudioArtistTap( - audio: audio, - ); - return; - case AudioType.radio: - _onRadioArtistTap(audio); - return; - case AudioType.podcast: - _onPodcastArtistTap( - audio: audio, - context: context, - ); - return; - default: - return; - } - } - - void _onRadioArtistTap(Audio audio) { - final libraryModel = di(); - if (audio.url == null) return; - libraryModel.push( - builder: (_) => StationPage(station: audio), - pageId: audio.url!, - ); - } - - void _onPodcastArtistTap({ - required Audio audio, - required BuildContext context, - }) { - final libraryModel = di(); - if (audio.website != null && - libraryModel.isPodcastSubscribed(audio.website)) { - libraryModel.pushNamed(pageId: audio.website!); - } else { - searchAndPushPodcastPage( - context: context, - feedUrl: audio.website, - itemImageUrl: audio.albumArtUrl, - genre: audio.genre, - play: false, - ); - } - } - - void _onLocalAudioArtistTap({required Audio audio}) { - final localAudioModel = di(); - - final artistName = audio.artist; - if (artistName == null) return; - - localAudioModel.init().then( - (value) { - final artistAudios = localAudioModel.findTitlesOfArtist(artistName); - final artist = artistAudios?.firstOrNull?.artist; - if (artist == null) return; - di().setFullWindowMode(false); - di().push( - builder: (_) => ArtistPage(artistAudios: artistAudios), - pageId: artist, - ); - }, - ); - } -} diff --git a/lib/player/view/bottom_player.dart b/lib/player/view/bottom_player.dart index 2e9c29d12..3f95dca81 100644 --- a/lib/player/view/bottom_player.dart +++ b/lib/player/view/bottom_player.dart @@ -12,18 +12,17 @@ import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../player/player_model.dart'; import 'bottom_player_image.dart'; -import 'bottom_player_title_artist.dart'; import 'play_button.dart'; import 'playback_rate_button.dart'; import 'player_main_controls.dart'; +import 'player_title_and_artist.dart'; import 'player_track.dart'; +import 'player_view.dart'; import 'queue_button.dart'; import 'volume_popup.dart'; class BottomPlayer extends StatelessWidget with WatchItMixin { - const BottomPlayer({ - super.key, - }); + const BottomPlayer({super.key}); @override Widget build(BuildContext context) { @@ -74,10 +73,10 @@ class BottomPlayer extends StatelessWidget with WatchItMixin { flex: 4, child: Row( children: [ - Flexible( + const Flexible( flex: 5, - child: BottomPlayerTitleArtist( - audio: audio, + child: PlayerTitleAndArtist( + playerPosition: PlayerPosition.bottom, ), ), if (!smallWindow) diff --git a/lib/player/view/bottom_player_title_artist.dart b/lib/player/view/bottom_player_title_artist.dart deleted file mode 100644 index b7328cbaa..000000000 --- a/lib/player/view/bottom_player_title_artist.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../common/data/audio.dart'; -import '../../common/view/theme.dart'; -import '../player_mixin.dart'; -import '../player_model.dart'; - -class BottomPlayerTitleArtist extends StatelessWidget - with WatchItMixin, PlayerMixin { - const BottomPlayerTitleArtist({ - super.key, - required this.audio, - }); - final Audio? audio; - - @override - Widget build(BuildContext context) { - final icyTitle = - watchPropertyValue((PlayerModel m) => m.mpvMetaData?.icyTitle); - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - borderRadius: BorderRadius.circular(4), - onTap: audio == null - ? null - : () => onTitleTap( - audio: audio, - text: icyTitle, - context: context, - ), - child: Tooltip( - message: getTitle(audio: audio, icyTitle: icyTitle), - child: Text( - getTitle(audio: audio, icyTitle: icyTitle), - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - InkWell( - borderRadius: BorderRadius.circular(4), - onTap: audio == null - ? null - : () => onArtistTap( - audio: audio!, - context: context, - ), - child: Tooltip( - message: getSubTitle(audio), - child: Text( - getSubTitle(audio), - style: TextStyle( - fontWeight: smallTextFontWeight, - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ), - ], - ); - } -} diff --git a/lib/player/view/full_height_player.dart b/lib/player/view/full_height_player.dart index 111009c7b..4ef611477 100644 --- a/lib/player/view/full_height_player.dart +++ b/lib/player/view/full_height_player.dart @@ -12,7 +12,7 @@ import '../../radio/view/radio_history_list.dart'; import 'blurred_full_height_player_image.dart'; import 'full_height_player_image.dart'; import 'full_height_player_top_controls.dart'; -import 'full_height_title_and_artist.dart'; +import 'player_title_and_artist.dart'; import 'full_height_video_player.dart'; import 'player_main_controls.dart'; import 'player_track.dart'; @@ -58,8 +58,8 @@ class FullHeightPlayer extends StatelessWidget with WatchItMixin { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30), - child: FullHeightTitleAndArtist( - audio: audio, + child: PlayerTitleAndArtist( + playerPosition: playerPosition, ), ), const SizedBox( diff --git a/lib/player/view/full_height_title_and_artist.dart b/lib/player/view/full_height_title_and_artist.dart deleted file mode 100644 index 7b771b47b..000000000 --- a/lib/player/view/full_height_title_and_artist.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../common/view/theme.dart'; -import '../../extensions/build_context_x.dart'; - -import '../../common/data/audio.dart'; -import '../player_model.dart'; -import '../player_mixin.dart'; - -class FullHeightTitleAndArtist extends StatelessWidget - with WatchItMixin, PlayerMixin { - const FullHeightTitleAndArtist({ - super.key, - required this.audio, - }); - - final Audio? audio; - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final icyTitle = - watchPropertyValue((PlayerModel m) => m.mpvMetaData?.icyTitle); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - borderRadius: BorderRadius.circular(10), - onTap: audio == null - ? null - : () => onTitleTap( - audio: audio!, - text: icyTitle, - context: context, - ), - child: Tooltip( - message: getTitle(audio: audio, icyTitle: icyTitle), - child: Text( - getTitle(audio: audio, icyTitle: icyTitle), - style: TextStyle( - fontWeight: largeTextWeight, - fontSize: 30, - color: theme.colorScheme.onSurface, - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - InkWell( - borderRadius: BorderRadius.circular(8), - onTap: audio == null - ? null - : () => onArtistTap( - audio: audio!, - context: context, - ), - child: Tooltip( - message: getSubTitle(audio), - child: Text( - getSubTitle(audio), - style: TextStyle( - fontWeight: smallTextFontWeight, - fontSize: 20, - color: theme.colorScheme.onSurface, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ), - ], - ); - } -} diff --git a/lib/player/view/player_title_and_artist.dart b/lib/player/view/player_title_and_artist.dart new file mode 100644 index 000000000..2e2a9c01b --- /dev/null +++ b/lib/player/view/player_title_and_artist.dart @@ -0,0 +1,308 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:watch_it/watch_it.dart'; + +import '../../app/app_model.dart'; +import '../../common/data/audio.dart'; +import '../../common/view/copy_clipboard_content.dart'; +import '../../common/view/snackbars.dart'; +import '../../common/view/theme.dart'; +import '../../extensions/build_context_x.dart'; +import '../../library/library_model.dart'; +import '../../local_audio/local_audio_model.dart'; +import '../../local_audio/view/album_page.dart'; +import '../../local_audio/view/artist_page.dart'; +import '../../podcasts/podcast_model.dart'; +import '../../radio/view/station_page.dart'; +import '../player_model.dart'; +import 'player_view.dart'; + +class PlayerTitleAndArtist extends StatelessWidget with WatchItMixin { + const PlayerTitleAndArtist({ + super.key, + required this.playerPosition, + }); + + final PlayerPosition playerPosition; + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final audio = watchPropertyValue((PlayerModel m) => m.audio); + + final icyTitle = + watchPropertyValue((PlayerModel m) => m.mpvMetaData?.icyTitle); + + final appModel = di(); + final libraryModel = di(); + final localAudioModel = di(); + final playerModel = di(); + final podcastModel = di(); + + return Column( + mainAxisSize: switch (playerPosition) { + PlayerPosition.bottom => MainAxisSize.max, + _ => MainAxisSize.min, + }, + mainAxisAlignment: switch (playerPosition) { + PlayerPosition.bottom => MainAxisAlignment.center, + _ => MainAxisAlignment.start, + }, + crossAxisAlignment: switch (playerPosition) { + PlayerPosition.bottom => CrossAxisAlignment.start, + _ => CrossAxisAlignment.center, + }, + children: [ + InkWell( + borderRadius: switch (playerPosition) { + PlayerPosition.bottom => BorderRadius.circular(4), + _ => BorderRadius.circular(10), + }, + onTap: audio == null + ? null + : () => _onTitleTap( + audio: audio, + text: icyTitle, + context: context, + libraryModel: libraryModel, + playerModel: playerModel, + localAudioModel: localAudioModel, + appModel: appModel, + ), + child: Tooltip( + message: _title(audio: audio, icyTitle: icyTitle), + child: Text( + _title(audio: audio, icyTitle: icyTitle), + style: switch (playerPosition) { + PlayerPosition.bottom => _bottomTitleTextStyle(), + _ => _fullHeightTitleTextStyle(theme) + }, + textAlign: _textAlign(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + InkWell( + borderRadius: switch (playerPosition) { + PlayerPosition.bottom => BorderRadius.circular(4), + _ => BorderRadius.circular(8), + }, + onTap: audio == null + ? null + : () => _onArtistTap( + audio: audio, + context: context, + libraryModel: libraryModel, + playerModel: playerModel, + localAudioModel: localAudioModel, + appModel: appModel, + podcastModel: podcastModel, + ), + child: Tooltip( + message: _subTitle(audio), + child: Text( + _subTitle(audio), + style: switch (playerPosition) { + PlayerPosition.bottom => _bottomArtistTextStyle(), + _ => _fullHeightArtistTextStyle(theme) + }, + textAlign: _textAlign(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + ], + ); + } + + TextAlign _textAlign() { + return switch (playerPosition) { + PlayerPosition.bottom => TextAlign.start, + _ => TextAlign.center, + }; + } + + TextStyle _fullHeightTitleTextStyle(ThemeData theme) { + return TextStyle( + fontWeight: largeTextWeight, + fontSize: 30, + color: theme.colorScheme.onSurface, + ); + } + + TextStyle _fullHeightArtistTextStyle(ThemeData theme) { + return TextStyle( + fontWeight: smallTextFontWeight, + fontSize: 20, + color: theme.colorScheme.onSurface, + ); + } + + TextStyle _bottomTitleTextStyle() => const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + ); + + TextStyle _bottomArtistTextStyle() => TextStyle( + fontWeight: smallTextFontWeight, + fontSize: 12, + ); + + String _subTitle(Audio? audio) => (switch (audio?.audioType) { + AudioType.podcast => audio?.album, + AudioType.radio => audio?.title, + _ => audio?.artist + } ?? + '') + .trim(); + + String _title({required Audio? audio, required String? icyTitle}) => + icyTitle?.isNotEmpty == true && audio?.audioType == AudioType.radio + ? icyTitle! + : (audio?.title?.isNotEmpty == true ? audio!.title! : '').trim(); + + void _onTitleTap({ + Audio? audio, + String? text, + required BuildContext context, + required PlayerModel playerModel, + required LibraryModel libraryModel, + required LocalAudioModel localAudioModel, + required AppModel appModel, + }) { + if (text?.isNotEmpty == true && audio?.audioType == AudioType.radio || + audio?.audioType == null) { + showSnackBar( + context: context, + content: CopyClipboardContent(text: text!), + ); + return; + } + + switch (audio?.audioType) { + case AudioType.local: + _onLocalAudioTitleTap( + audio: audio!, + appModel: appModel, + libraryModel: libraryModel, + localAudioModel: localAudioModel, + ); + return; + case AudioType.radio: + case AudioType.podcast: + if (audio?.url == null) return; + showSnackBar( + context: context, + content: CopyClipboardContent( + text: audio!.url!, + onSearch: () => launchUrl(Uri.parse(audio.url!)), + ), + ); + return; + default: + return; + } + } + + void _onLocalAudioTitleTap({ + required Audio audio, + required AppModel appModel, + required LibraryModel libraryModel, + required LocalAudioModel localAudioModel, + }) { + if (audio.album == null) return; + localAudioModel.init().then( + (value) { + final albumAudios = localAudioModel.findAlbum(audio.album!); + if (albumAudios?.firstOrNull == null) return; + final id = albumAudios!.first.albumId; + if (id == null) return; + + appModel.setFullWindowMode(false); + + libraryModel.push( + builder: (_) => AlbumPage( + id: id, + album: albumAudios, + ), + pageId: id, + ); + }, + ); + } + + void _onArtistTap({ + required Audio audio, + required BuildContext context, + required PodcastModel podcastModel, + required PlayerModel playerModel, + required LocalAudioModel localAudioModel, + required LibraryModel libraryModel, + required AppModel appModel, + }) { + switch (audio.audioType) { + case AudioType.local: + _onLocalAudioArtistTap( + audio: audio, + appModel: appModel, + libraryModel: libraryModel, + localAudioModel: localAudioModel, + ); + return; + case AudioType.radio: + _onRadioArtistTap(audio: audio, libraryModel: libraryModel); + return; + case AudioType.podcast: + if (audio.website != null) { + podcastModel.loadPodcast( + context: context, + libraryModel: libraryModel, + feedUrl: audio.website!, + itemImageUrl: audio.albumArtUrl, + genre: audio.genre, + ); + } + return; + default: + return; + } + } + + void _onRadioArtistTap({ + required Audio audio, + required LibraryModel libraryModel, + }) { + if (audio.url == null || + audio.uuid == null || + libraryModel.selectedPageId == audio.uuid) return; + libraryModel.push( + builder: (_) => StationPage(station: audio), + pageId: audio.uuid!, + ); + } + + void _onLocalAudioArtistTap({ + required Audio audio, + required LocalAudioModel localAudioModel, + required AppModel appModel, + required LibraryModel libraryModel, + }) { + final artistName = audio.artist; + if (artistName == null) return; + + localAudioModel.init().then( + (value) { + final artistAudios = localAudioModel.findTitlesOfArtist(artistName); + final artist = artistAudios?.firstOrNull?.artist; + if (artist == null) return; + appModel.setFullWindowMode(false); + libraryModel.push( + builder: (_) => ArtistPage(artistAudios: artistAudios), + pageId: artist, + ); + }, + ); + } +} diff --git a/lib/player/view/player_view.dart b/lib/player/view/player_view.dart index eede569a3..21a4ca82e 100644 --- a/lib/player/view/player_view.dart +++ b/lib/player/view/player_view.dart @@ -9,9 +9,9 @@ import 'bottom_player.dart'; import 'full_height_player.dart'; class PlayerView extends StatefulWidget with WatchItStatefulWidgetMixin { - const PlayerView({super.key, required this.mode}); + const PlayerView({super.key, required this.position}); - final PlayerPosition mode; + final PlayerPosition position; @override State createState() => _PlayerViewState(); @@ -25,7 +25,7 @@ class _PlayerViewState extends State { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (!mounted) return; di().setShowWindowControls( - widget.mode != PlayerPosition.sideBar, + widget.position != PlayerPosition.sideBar, ); }); } @@ -37,7 +37,7 @@ class _PlayerViewState extends State { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (!mounted) return; di().setShowWindowControls( - widget.mode != PlayerPosition.sideBar, + widget.position != PlayerPosition.sideBar, ); }); } @@ -49,13 +49,13 @@ class _PlayerViewState extends State { final color = getPlayerBg(c, theme.cardColor); Widget player; - if (widget.mode != PlayerPosition.bottom) { + if (widget.position != PlayerPosition.bottom) { player = Row( children: [ - if (widget.mode == PlayerPosition.sideBar) + if (widget.position == PlayerPosition.sideBar) const Material(child: VerticalDivider()), Expanded( - child: FullHeightPlayer(playerPosition: widget.mode), + child: FullHeightPlayer(playerPosition: widget.position), ), ], ); diff --git a/lib/playlists/view/manual_add_dialog.dart b/lib/playlists/view/manual_add_dialog.dart index be94dd385..46fc058ed 100644 --- a/lib/playlists/view/manual_add_dialog.dart +++ b/lib/playlists/view/manual_add_dialog.dart @@ -10,7 +10,7 @@ import '../../common/view/theme.dart'; import '../../external_path/external_path_service.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; -import '../../podcasts/podcast_utils.dart'; +import '../../podcasts/podcast_model.dart'; class ManualAddDialog extends StatelessWidget { const ManualAddDialog({super.key}); @@ -392,10 +392,10 @@ class _AddPodcastContentState extends State { onPressed: _urlController.text.isEmpty ? null : () { - searchAndPushPodcastPage( + di().loadPodcast( context: context, feedUrl: _urlController.text, - play: false, + libraryModel: di(), ); }, child: Text( diff --git a/lib/podcasts/podcast_model.dart b/lib/podcasts/podcast_model.dart index df0a71748..3789b442f 100644 --- a/lib/podcasts/podcast_model.dart +++ b/lib/podcasts/podcast_model.dart @@ -1,9 +1,15 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../common/data/audio.dart'; +import '../common/view/snackbars.dart'; +import '../library/library_model.dart'; +import '../player/player_model.dart'; import 'podcast_service.dart'; +import 'view/podcast_page.dart'; +import 'view/podcast_snackbar_contents.dart'; class PodcastModel extends SafeChangeNotifier { PodcastModel({ @@ -71,4 +77,78 @@ class PodcastModel extends SafeChangeNotifier { _downloadsOnly = value; notifyListeners(); } + + // This is not optimal since the model uses widgets + // but tolerable since snackbars update over the rest of the UI + Future loadPodcast({ + required BuildContext context, + required LibraryModel libraryModel, + required String feedUrl, + String? itemImageUrl, + String? genre, + PlayerModel? playerModel, + }) async { + if (libraryModel.isPageInLibrary(feedUrl)) { + return libraryModel.pushNamed(pageId: feedUrl); + } + + showSnackBar( + context: context, + duration: const Duration(seconds: 1000), + content: const PodcastSearchLoadingSnackBarContent(), + ); + + setLoadingFeed(true); + return _podcastService + .findEpisodes( + feedUrl: feedUrl, + itemImageUrl: itemImageUrl, + genre: genre, + ) + .then( + (podcast) async { + if (podcast.isEmpty) { + if (context.mounted) { + showSnackBar( + context: context, + content: const PodcastSearchEmptyFeedSnackBarContent(), + ); + } + return; + } + + if (playerModel != null) { + playerModel.startPlaylist(listName: feedUrl, audios: podcast); + } else { + libraryModel.push( + builder: (_) => PodcastPage( + imageUrl: itemImageUrl ?? podcast.firstOrNull?.imageUrl, + preFetchedEpisodes: podcast, + feedUrl: feedUrl, + title: podcast.firstOrNull?.album ?? + podcast.firstOrNull?.title ?? + feedUrl, + ), + pageId: feedUrl, + ); + } + }, + ).whenComplete( + () { + setLoadingFeed(false); + if (context.mounted) ScaffoldMessenger.of(context).clearSnackBars(); + }, + ).timeout( + const Duration(seconds: 15), + onTimeout: () { + setLoadingFeed(false); + if (context.mounted) { + showSnackBar( + context: context, + content: const PodcastSearchTimeoutSnackBarContent(), + ); + } + }, + ); + } } diff --git a/lib/podcasts/podcast_service.dart b/lib/podcasts/podcast_service.dart index b783a4919..fbc2dbc88 100644 --- a/lib/podcasts/podcast_service.dart +++ b/lib/podcasts/podcast_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:podcast_search/podcast_search.dart'; import '../common/data/audio.dart'; @@ -9,7 +10,6 @@ import '../common/view/languages.dart'; import '../library/library_service.dart'; import '../notifications/notifications_service.dart'; import '../settings/settings_service.dart'; -import 'podcast_utils.dart'; class PodcastService { final NotificationsService _notificationsService; @@ -125,4 +125,42 @@ class PodcastService { } _updateLock = false; } + + Future> findEpisodes({ + required String feedUrl, + String? itemImageUrl, + String? genre, + }) async { + final Podcast? podcast = await compute(loadPodcast, feedUrl); + final episodes = podcast?.episodes + .where((e) => e.contentUrl != null) + .map( + (e) => Audio.fromPodcast( + episode: e, + podcast: podcast, + itemImageUrl: itemImageUrl, + genre: genre, + ), + ) + .toList() ?? +