Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: remove build context and snackbars from podcast model #1085

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/app/view/desktop_home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -77,6 +80,35 @@ class _DesktopHomePageState extends State<DesktopHomePage> {
},
);

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: [
Expand Down
32 changes: 32 additions & 0 deletions lib/app/view/mobile_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions lib/library/library_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 24 additions & 12 deletions lib/player/view/player_title_and_artist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
14 changes: 12 additions & 2 deletions lib/playlists/view/manual_add_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -425,9 +426,18 @@ class _AddPodcastContentState extends State<AddPodcastContent> {
? null
: () {
di<PodcastModel>().loadPodcast(
context: context,
feedUrl: _urlController.text,
libraryModel: di<LibraryModel>(),
onFind: (podcast) => di<LibraryModel>().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(
Expand Down
114 changes: 40 additions & 74 deletions lib/podcasts/podcast_model.dart
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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<PodcastSearchState>.broadcast();
Stream<PodcastSearchState> 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;
Expand Down Expand Up @@ -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<void> loadPodcast({
required BuildContext context,
required LibraryModel libraryModel,
required String feedUrl,
String? itemImageUrl,
String? genre,
PlayerModel? playerModel,
required Function(List<Audio> podcast) onFind,
}) async {
if (libraryModel.isPageInLibrary(feedUrl)) {
return libraryModel.push(pageId: feedUrl);
}

showSnackBar(
context: context,
duration: const Duration(seconds: 1000),
content: const PodcastSearchLoadingSnackBarContent(),
);
_sendState(PodcastSearchState.loading);

setLoadingFeed(true);
return _podcastService
.findEpisodes(
feedUrl: feedUrl,
itemImageUrl: itemImageUrl,
genre: genre,
)
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(),
);
}
},
);
(podcast) async {
if (podcast.isEmpty) {
_sendState(PodcastSearchState.empty);
return;
}

onFind(podcast);
},
)
.whenComplete(
() => _sendState(PodcastSearchState.done),
)
.timeout(
const Duration(seconds: 15),
onTimeout: () {
_sendState(PodcastSearchState.timeout);
},
);
}

@override
Future<void> dispose() async {
super.dispose();
await _searchStateController.close();
}
}
6 changes: 6 additions & 0 deletions lib/podcasts/podcast_search_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum PodcastSearchState {
loading,
timeout,
empty,
done;
}
1 change: 1 addition & 0 deletions lib/podcasts/view/podcast_sub_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class PodcastSubButton extends StatelessWidget with WatchItMixin {
: () {
if (subscribed) {
libraryModel.removePodcast(pageId);
libraryModel.pop();
} else if (audios.isNotEmpty) {
libraryModel.addPodcast(pageId, audios);
}
Expand Down
Loading
Loading