diff --git a/lib/app/view/desktop_home_page.dart b/lib/app/view/desktop_home_page.dart index 92132b05..124cfb88 100644 --- a/lib/app/view/desktop_home_page.dart +++ b/lib/app/view/desktop_home_page.dart @@ -13,6 +13,9 @@ import '../../patch_notes/patch_notes_dialog.dart'; import '../../player/player_model.dart'; import '../../player/view/player_view.dart'; import '../../podcasts/download_model.dart'; +import '../../podcasts/podcast_model.dart'; +import '../../podcasts/podcast_search_state.dart'; +import '../../podcasts/view/podcast_snackbar_contents.dart'; import '../../settings/settings_model.dart'; import '../app_model.dart'; import '../connectivity_model.dart'; @@ -77,6 +80,35 @@ class _DesktopHomePageState extends State { }, ); + registerStreamHandler( + select: (PodcastModel m) => m.stateStream, + initialValue: null, + handler: (context, newValue, cancel) { + if (newValue.hasData) { + if (newValue.data == PodcastSearchState.done) { + ScaffoldMessenger.of(context).clearSnackBars(); + } else { + showSnackBar( + context: context, + content: switch (newValue.data) { + PodcastSearchState.loading => + const PodcastSearchLoadingSnackBarContent(), + PodcastSearchState.empty => + const PodcastSearchEmptyFeedSnackBarContent(), + PodcastSearchState.timeout => + const PodcastSearchTimeoutSnackBarContent(), + _ => const SizedBox.shrink() + }, + duration: switch (newValue.data) { + PodcastSearchState.loading => const Duration(seconds: 1000), + _ => const Duration(seconds: 3), + }, + ); + } + } + }, + ); + return Stack( alignment: Alignment.center, children: [ diff --git a/lib/app/view/mobile_page.dart b/lib/app/view/mobile_page.dart index 38ef53a9..df7eac9b 100644 --- a/lib/app/view/mobile_page.dart +++ b/lib/app/view/mobile_page.dart @@ -8,6 +8,9 @@ import '../../player/player_model.dart'; import '../../player/view/full_height_player.dart'; import '../../player/view/player_view.dart'; import '../../podcasts/download_model.dart'; +import '../../podcasts/podcast_model.dart'; +import '../../podcasts/podcast_search_state.dart'; +import '../../podcasts/view/podcast_snackbar_contents.dart'; import '../app_model.dart'; import 'mobile_bottom_bar.dart'; @@ -34,6 +37,35 @@ class MobilePage extends StatelessWidget with WatchItMixin { }, ); + registerStreamHandler( + select: (PodcastModel m) => m.stateStream, + initialValue: null, + handler: (context, newValue, cancel) { + if (newValue.hasData) { + if (newValue.data == PodcastSearchState.done) { + ScaffoldMessenger.of(context).clearSnackBars(); + } else { + showSnackBar( + context: context, + content: switch (newValue.data) { + PodcastSearchState.loading => + const PodcastSearchLoadingSnackBarContent(), + PodcastSearchState.empty => + const PodcastSearchEmptyFeedSnackBarContent(), + PodcastSearchState.timeout => + const PodcastSearchTimeoutSnackBarContent(), + _ => const SizedBox.shrink() + }, + duration: switch (newValue.data) { + PodcastSearchState.loading => const Duration(seconds: 1000), + _ => const Duration(seconds: 3), + }, + ); + } + } + }, + ); + return Scaffold( resizeToAvoidBottomInset: false, extendBody: true, diff --git a/lib/library/library_model.dart b/lib/library/library_model.dart index 618c79f1..7c6aad65 100644 --- a/lib/library/library_model.dart +++ b/lib/library/library_model.dart @@ -262,6 +262,9 @@ class LibraryModel extends SafeChangeNotifier implements NavigatorObserver { printMessageInDebugMode( 'didReplace: ${oldRoute?.settings.name}, newPageId: ${newRoute?.settings.name}', ); + final pageId = newRoute?.settings.name; + if (pageId == null) return; + _setSelectedPageId(pageId); } @override diff --git a/lib/player/view/player_title_and_artist.dart b/lib/player/view/player_title_and_artist.dart index 44f6c24d..128ab9ad 100644 --- a/lib/player/view/player_title_and_artist.dart +++ b/lib/player/view/player_title_and_artist.dart @@ -14,6 +14,7 @@ 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 '../../podcasts/view/podcast_page.dart'; import '../../radio/view/station_page.dart'; import '../../settings/settings_model.dart'; import '../player_model.dart'; @@ -97,9 +98,7 @@ class PlayerTitleAndArtist extends StatelessWidget with WatchItMixin { ? null : () => _onArtistTap( audio: audio, - context: context, libraryModel: libraryModel, - playerModel: playerModel, localAudioModel: localAudioModel, appModel: appModel, podcastModel: podcastModel, @@ -254,9 +253,7 @@ class PlayerTitleAndArtist extends StatelessWidget with WatchItMixin { void _onArtistTap({ required Audio audio, - required BuildContext context, required PodcastModel podcastModel, - required PlayerModel playerModel, required LocalAudioModel localAudioModel, required LibraryModel libraryModel, required AppModel appModel, @@ -274,14 +271,29 @@ class PlayerTitleAndArtist extends StatelessWidget with WatchItMixin { _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, - ); + if (audio.website != null && + libraryModel.selectedPageId != audio.website) { + final feedUrl = audio.website!; + if (libraryModel.isPageInLibrary(feedUrl)) { + libraryModel.push(pageId: feedUrl); + } else { + podcastModel.loadPodcast( + feedUrl: feedUrl, + itemImageUrl: audio.albumArtUrl, + genre: audio.genre, + onFind: (podcast) => libraryModel.push( + builder: (_) => PodcastPage( + imageUrl: audio.albumArtUrl ?? podcast.firstOrNull?.imageUrl, + preFetchedEpisodes: podcast, + feedUrl: feedUrl, + title: podcast.firstOrNull?.album ?? + podcast.firstOrNull?.title ?? + feedUrl, + ), + pageId: feedUrl, + ), + ); + } } return; default: diff --git a/lib/playlists/view/manual_add_dialog.dart b/lib/playlists/view/manual_add_dialog.dart index b244a545..9d37e232 100644 --- a/lib/playlists/view/manual_add_dialog.dart +++ b/lib/playlists/view/manual_add_dialog.dart @@ -15,6 +15,7 @@ import '../../library/library_model.dart'; import '../../local_audio/local_audio_model.dart'; import '../../local_audio/local_audio_view.dart'; import '../../podcasts/podcast_model.dart'; +import '../../podcasts/view/podcast_page.dart'; class ManualAddDialog extends StatelessWidget { const ManualAddDialog({ @@ -425,9 +426,18 @@ class _AddPodcastContentState extends State { ? null : () { di().loadPodcast( - context: context, feedUrl: _urlController.text, - libraryModel: di(), + onFind: (podcast) => di().push( + builder: (_) => PodcastPage( + imageUrl: podcast.firstOrNull?.imageUrl, + preFetchedEpisodes: podcast, + feedUrl: _urlController.text, + title: podcast.firstOrNull?.album ?? + podcast.firstOrNull?.title ?? + _urlController.text, + ), + pageId: _urlController.text, + ), ); }, child: Text( diff --git a/lib/podcasts/podcast_model.dart b/lib/podcasts/podcast_model.dart index 6a73dafa..b01fc82c 100644 --- a/lib/podcasts/podcast_model.dart +++ b/lib/podcasts/podcast_model.dart @@ -1,15 +1,10 @@ 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_search_state.dart'; import 'podcast_service.dart'; -import 'view/podcast_page.dart'; -import 'view/podcast_snackbar_contents.dart'; class PodcastModel extends SafeChangeNotifier { PodcastModel({ @@ -18,12 +13,16 @@ class PodcastModel extends SafeChangeNotifier { final PodcastService _podcastService; - bool _loadingFeed = false; - bool get loadingFeed => _loadingFeed; - void setLoadingFeed(bool value) { - if (_loadingFeed == value) return; - _loadingFeed = value; + final _searchStateController = + StreamController.broadcast(); + Stream get stateStream => _searchStateController.stream; + PodcastSearchState _lastState = PodcastSearchState.done; + PodcastSearchState get lastState => _lastState; + void _sendState(PodcastSearchState state) { + if (state == _lastState) return; + _lastState = state; notifyListeners(); + _searchStateController.add(state); } var _firstUpdateChecked = false; @@ -78,77 +77,44 @@ class PodcastModel extends SafeChangeNotifier { 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, + required Function(List