added cargo files

This commit is contained in:
2026-03-03 10:57:43 -05:00
parent 478a90e01b
commit 169df46bc2
813 changed files with 227273 additions and 9 deletions

View File

@@ -0,0 +1,238 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:pinepods_mobile/bloc/bloc.dart';
import 'package:pinepods_mobile/core/extensions.dart';
import 'package:pinepods_mobile/entities/episode.dart';
import 'package:pinepods_mobile/entities/sleep.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/state/transcript_state_event.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
enum TransitionState {
play,
pause,
stop,
fastforward,
rewind,
}
enum LifecycleState {
pause,
resume,
detach,
}
/// A BLoC to handle interactions between the audio service and the client.
class AudioBloc extends Bloc {
final log = Logger('AudioBloc');
/// Listen for new episode play requests.
final BehaviorSubject<Episode?> _play = BehaviorSubject<Episode?>();
/// Move from one playing state to another such as from paused to play
final PublishSubject<TransitionState> _transitionPlayingState = PublishSubject<TransitionState>();
/// Sink to update our position
final PublishSubject<double> _transitionPosition = PublishSubject<double>();
/// Handles persisting data to storage.
final AudioPlayerService audioPlayerService;
/// Listens for playback speed change requests.
final PublishSubject<double> _playbackSpeedSubject = PublishSubject<double>();
/// Listen for toggling of trim silence requests.
final PublishSubject<bool> _trimSilence = PublishSubject<bool>();
/// Listen for toggling of volume boost silence requests.
final PublishSubject<bool> _volumeBoost = PublishSubject<bool>();
/// Listen for transcript filtering events.
final PublishSubject<TranscriptEvent> _transcriptEvent = PublishSubject<TranscriptEvent>();
final BehaviorSubject<Sleep> _sleepEvent = BehaviorSubject<Sleep>();
AudioBloc({
required this.audioPlayerService,
}) {
/// Listen for transition events from the client.
_handlePlayingStateTransitions();
/// Listen for events requesting the start of a new episode.
_handleEpisodeRequests();
/// Listen for requests to move the play position within the episode.
_handlePositionTransitions();
/// Listen for playback speed changes
_handlePlaybackSpeedTransitions();
/// Listen to trim silence requests
_handleTrimSilenceTransitions();
/// Listen to volume boost silence requests
_handleVolumeBoostTransitions();
/// Listen to transcript filtering events
_handleTranscriptEvents();
/// Listen to sleep timer events;
_handleSleepTimer();
}
/// Listens to events from the UI (or any client) to transition from one
/// audio state to another. For example, to pause the current playback
/// a [TransitionState.pause] event should be sent. To ensure the underlying
/// audio service processes one state request at a time we push events
/// on to a queue and execute them sequentially. Each state maps to a call
/// to the Audio Service plugin.
void _handlePlayingStateTransitions() {
_transitionPlayingState.asyncMap((event) => Future.value(event)).listen((state) async {
switch (state) {
case TransitionState.play:
await audioPlayerService.play();
break;
case TransitionState.pause:
await audioPlayerService.pause();
break;
case TransitionState.fastforward:
await audioPlayerService.fastForward();
break;
case TransitionState.rewind:
await audioPlayerService.rewind();
break;
case TransitionState.stop:
await audioPlayerService.stop();
break;
}
});
}
/// Setup a listener for episode requests and then connect to the
/// underlying audio service.
void _handleEpisodeRequests() async {
_play.listen((episode) {
audioPlayerService.playEpisode(episode: episode!, resume: true);
});
}
/// Listen for requests to change the position of the current episode.
void _handlePositionTransitions() async {
_transitionPosition.listen((pos) async {
await audioPlayerService.seek(position: pos.ceil());
});
}
/// Listen for requests to adjust the playback speed.
void _handlePlaybackSpeedTransitions() {
_playbackSpeedSubject.listen((double speed) async {
await audioPlayerService.setPlaybackSpeed(speed.toTenth);
});
}
/// Listen for requests to toggle trim silence mode. This is currently disabled until
/// [issue](https://github.com/ryanheise/just_audio/issues/558) is resolved.
void _handleTrimSilenceTransitions() {
_trimSilence.listen((bool trim) async {
await audioPlayerService.trimSilence(trim);
});
}
/// Listen for requests to toggle the volume boost feature. Android only.
void _handleVolumeBoostTransitions() {
_volumeBoost.listen((bool boost) async {
await audioPlayerService.volumeBoost(boost);
});
}
void _handleTranscriptEvents() {
_transcriptEvent.listen((TranscriptEvent event) {
if (event is TranscriptFilterEvent) {
audioPlayerService.searchTranscript(event.search);
} else if (event is TranscriptClearEvent) {
audioPlayerService.clearTranscript();
}
});
}
void _handleSleepTimer() {
_sleepEvent.listen((Sleep sleep) {
audioPlayerService.sleep(sleep);
});
}
@override
void pause() async {
log.fine('Audio lifecycle pause');
await audioPlayerService.suspend();
}
@override
void resume() async {
log.fine('Audio lifecycle resume');
var ep = await audioPlayerService.resume();
if (ep != null) {
log.fine('Resuming with episode ${ep.title} - ${ep.position} - ${ep.played}');
} else {
log.fine('Resuming without an episode');
}
}
/// Play the specified track now
void Function(Episode?) get play => _play.add;
/// Transition the state from connecting, to play, pause, stop etc.
void Function(TransitionState) get transitionState => _transitionPlayingState.add;
/// Move the play position.
void Function(double) get transitionPosition => _transitionPosition.sink.add;
/// Get the current playing state
Stream<AudioState>? get playingState => audioPlayerService.playingState;
/// Listen for any playback errors
Stream<int>? get playbackError => audioPlayerService.playbackError;
/// Get the current playing episode
ValueStream<Episode?>? get nowPlaying => audioPlayerService.episodeEvent;
/// Get the current transcript (if there is one).
Stream<TranscriptState>? get nowPlayingTranscript => audioPlayerService.transcriptEvent;
/// Get position and percentage played of playing episode
ValueStream<PositionState>? get playPosition => audioPlayerService.playPosition;
Stream<Sleep>? get sleepStream => audioPlayerService.sleepStream;
/// Change playback speed
void Function(double) get playbackSpeed => _playbackSpeedSubject.sink.add;
/// Toggle trim silence
void Function(bool) get trimSilence => _trimSilence.sink.add;
/// Toggle volume boost silence
void Function(bool) get volumeBoost => _volumeBoost.sink.add;
/// Handle filtering & searching of the current transcript.
void Function(TranscriptEvent) get filterTranscript => _transcriptEvent.sink.add;
void Function(Sleep) get sleep => _sleepEvent.sink.add;
@override
void dispose() {
_play.close();
_transitionPlayingState.close();
_transitionPosition.close();
_playbackSpeedSubject.close();
_trimSilence.close();
_volumeBoost.close();
super.dispose();
}
}

View File

@@ -0,0 +1,125 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:pinepods_mobile/bloc/bloc.dart';
import 'package:pinepods_mobile/entities/episode.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/services/podcast/podcast_service.dart';
import 'package:pinepods_mobile/state/bloc_state.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
/// The BLoC provides access to [Episode] details outside the direct scope
/// of a [Podcast].
class EpisodeBloc extends Bloc {
final log = Logger('EpisodeBloc');
final PodcastService podcastService;
final AudioPlayerService audioPlayerService;
/// Add to sink to fetch list of current downloaded episodes.
final BehaviorSubject<bool> _downloadsInput = BehaviorSubject<bool>();
/// Add to sink to fetch list of current episodes.
final BehaviorSubject<bool> _episodesInput = BehaviorSubject<bool>();
/// Add to sink to delete the passed [Episode] from storage.
final PublishSubject<Episode?> _deleteDownload = PublishSubject<Episode>();
/// Add to sink to toggle played status of the [Episode].
final PublishSubject<Episode?> _togglePlayed = PublishSubject<Episode>();
/// Stream of currently downloaded episodes
Stream<BlocState<List<Episode>>>? _downloadsOutput;
/// Stream of current episodes
Stream<BlocState<List<Episode>>>? _episodesOutput;
/// Cache of our currently downloaded episodes.
List<Episode>? _episodes;
EpisodeBloc({
required this.podcastService,
required this.audioPlayerService,
}) {
_init();
}
void _init() {
_downloadsOutput = _downloadsInput.switchMap<BlocState<List<Episode>>>((bool silent) => _loadDownloads(silent));
_episodesOutput = _episodesInput.switchMap<BlocState<List<Episode>>>((bool silent) => _loadEpisodes(silent));
_handleDeleteDownloads();
_handleMarkAsPlayed();
_listenEpisodeEvents();
}
void _handleDeleteDownloads() async {
_deleteDownload.stream.listen((episode) async {
var nowPlaying = audioPlayerService.nowPlaying?.guid == episode?.guid;
/// If we are attempting to delete the episode we are currently playing, we need to stop the audio.
if (nowPlaying) {
await audioPlayerService.stop();
}
await podcastService.deleteDownload(episode!);
fetchDownloads(true);
});
}
void _handleMarkAsPlayed() async {
_togglePlayed.stream.listen((episode) async {
await podcastService.toggleEpisodePlayed(episode!);
fetchDownloads(true);
});
}
void _listenEpisodeEvents() {
// Listen for episode updates. If the episode is downloaded, we need to update.
podcastService.episodeListener!.where((event) => event.episode.downloaded || event.episode.played).listen((event) => fetchDownloads(true));
}
Stream<BlocState<List<Episode>>> _loadDownloads(bool silent) async* {
if (!silent) {
yield BlocLoadingState();
}
_episodes = await podcastService.loadDownloads();
yield BlocPopulatedState<List<Episode>>(results: _episodes);
}
Stream<BlocState<List<Episode>>> _loadEpisodes(bool silent) async* {
if (!silent) {
yield BlocLoadingState();
}
_episodes = await podcastService.loadEpisodes();
yield BlocPopulatedState<List<Episode>>(results: _episodes);
}
@override
void dispose() {
_downloadsInput.close();
_deleteDownload.close();
_togglePlayed.close();
}
void Function(bool) get fetchDownloads => _downloadsInput.add;
void Function(bool) get fetchEpisodes => _episodesInput.add;
Stream<BlocState<List<Episode>>>? get downloads => _downloadsOutput;
Stream<BlocState<List<Episode>>>? get episodes => _episodesOutput;
void Function(Episode?) get deleteDownload => _deleteDownload.add;
void Function(Episode?) get togglePlayed => _togglePlayed.add;
}

View File

@@ -0,0 +1,517 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pinepods_mobile/bloc/bloc.dart';
import 'package:pinepods_mobile/entities/downloadable.dart';
import 'package:pinepods_mobile/entities/episode.dart';
import 'package:pinepods_mobile/entities/feed.dart';
import 'package:pinepods_mobile/entities/podcast.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/services/download/download_service.dart';
import 'package:pinepods_mobile/services/download/mobile_download_service.dart';
import 'package:pinepods_mobile/services/podcast/podcast_service.dart';
import 'package:pinepods_mobile/services/settings/settings_service.dart';
import 'package:pinepods_mobile/services/pinepods/pinepods_service.dart';
import 'package:pinepods_mobile/entities/pinepods_search.dart';
import 'package:pinepods_mobile/state/bloc_state.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
enum PodcastEvent {
subscribe,
unsubscribe,
markAllPlayed,
clearAllPlayed,
reloadSubscriptions,
refresh,
// Filter
episodeFilterNone,
episodeFilterStarted,
episodeFilterNotFinished,
episodeFilterFinished,
// Sort
episodeSortDefault,
episodeSortLatest,
episodeSortEarliest,
episodeSortAlphabeticalAscending,
episodeSortAlphabeticalDescending,
}
/// This BLoC provides access to the details of a given Podcast.
///
/// It takes a feed URL and creates a [Podcast] instance. There are several listeners that
/// handle actions on a podcast such as requesting an episode download, following/unfollowing
/// a podcast and marking/un-marking all episodes as played.
class PodcastBloc extends Bloc {
final log = Logger('PodcastBloc');
final PodcastService podcastService;
final AudioPlayerService audioPlayerService;
final DownloadService downloadService;
final SettingsService settingsService;
final BehaviorSubject<Feed> _podcastFeed = BehaviorSubject<Feed>(sync: true);
/// Add to sink to start an Episode download
final PublishSubject<Episode?> _downloadEpisode = PublishSubject<Episode?>();
/// Listen to this subject's stream to obtain list of current subscriptions.
late PublishSubject<List<Podcast>> _subscriptions;
/// Stream containing details of the current podcast.
final BehaviorSubject<BlocState<Podcast>> _podcastStream = BehaviorSubject<BlocState<Podcast>>(sync: true);
/// A separate stream that allows us to listen to changes in the podcast's episodes.
final BehaviorSubject<List<Episode?>?> _episodesStream = BehaviorSubject<List<Episode?>?>();
/// Receives subscription and mark/clear as played events.
final PublishSubject<PodcastEvent> _podcastEvent = PublishSubject<PodcastEvent>();
final BehaviorSubject<String> _podcastSearchEvent = BehaviorSubject<String>();
final BehaviorSubject<BlocState<void>> _backgroundLoadStream = BehaviorSubject<BlocState<void>>();
Podcast? _podcast;
List<Episode> _episodes = <Episode>[];
String _searchTerm = '';
late Feed lastFeed;
bool first = true;
PodcastBloc({
required this.podcastService,
required this.audioPlayerService,
required this.downloadService,
required this.settingsService,
}) {
_init();
}
void _init() {
/// When someone starts listening for subscriptions, load them.
_subscriptions = PublishSubject<List<Podcast>>(onListen: _loadSubscriptions);
/// When we receive a load podcast request, send back a BlocState.
_listenPodcastLoad();
/// Listen to an Episode download request
_listenDownloadRequest();
/// Listen to active downloads
_listenDownloads();
/// Listen to episode change events sent by the [Repository]
_listenEpisodeRepositoryEvents();
/// Listen to Podcast subscription, mark/cleared played events
_listenPodcastStateEvents();
/// Listen for episode search requests
_listenPodcastSearchEvents();
}
void _loadSubscriptions() async {
_subscriptions.add(await podcastService.subscriptions());
}
/// Sets up a listener to handle Podcast load requests. We first push a [BlocLoadingState] to
/// indicate that the Podcast is being loaded, before calling the [PodcastService] to handle
/// the loading. Once loaded, we extract the episodes from the Podcast and push them out via
/// the episode stream before pushing a [BlocPopulatedState] containing the Podcast.
void _listenPodcastLoad() async {
_podcastFeed.listen((feed) async {
var silent = false;
lastFeed = feed;
_episodes = [];
_refresh();
_podcastStream.sink.add(BlocLoadingState<Podcast>(feed.podcast));
try {
await _loadEpisodes(feed, feed.refresh);
/// Do we also need to perform a background refresh?
if (feed.podcast.id != null && feed.backgroundFresh && _shouldAutoRefresh()) {
silent = feed.silently;
log.fine('Performing background refresh of ${feed.podcast.url}');
_backgroundLoadStream.sink.add(BlocLoadingState<void>());
await _loadNewEpisodes(feed);
}
_backgroundLoadStream.sink.add(BlocSuccessfulState<void>());
} catch (e) {
_backgroundLoadStream.sink.add(BlocDefaultState<void>());
// For now we'll assume a network error as this is the most likely.
if ((_podcast == null || lastFeed.podcast.url == _podcast!.url) && !silent) {
_podcastStream.sink.add(BlocErrorState<Podcast>());
log.fine('Error loading podcast', e);
log.fine(e);
}
}
});
}
/// Determines if the current feed should be updated in the background.
///
/// If the autoUpdatePeriod is -1 this means never; 0 means always and any other
/// value is the time in minutes.
bool _shouldAutoRefresh() {
/// If we are currently following this podcast it will have an id. At
/// this point we can compare the last updated time to the update
/// after setting time.
if (settingsService.autoUpdateEpisodePeriod == -1) {
return false;
} else if (_podcast == null || settingsService.autoUpdateEpisodePeriod == 0) {
return true;
} else if (_podcast != null && _podcast!.id != null) {
var currentTime = DateTime.now().subtract(Duration(minutes: settingsService.autoUpdateEpisodePeriod));
var lastUpdated = _podcast!.lastUpdated;
return currentTime.isAfter(lastUpdated);
}
return false;
}
Future<void> _loadEpisodes(Feed feed, bool force) async {
_podcast = await podcastService.loadPodcast(
podcast: feed.podcast,
refresh: force,
);
/// Only populate episodes if the ID we started the load with is the
/// same as the one we have ended up with.
if (_podcast != null && _podcast?.url != null) {
if (lastFeed.podcast.url == _podcast!.url) {
_episodes = _podcast!.episodes;
_refresh();
_podcastStream.sink.add(BlocPopulatedState<Podcast>(results: _podcast));
}
}
}
void _refresh() {
applySearchFilter();
}
Future<void> _loadNewEpisodes(Feed feed) async {
_podcast = await podcastService.loadPodcast(
podcast: feed.podcast,
highlightNewEpisodes: true,
refresh: true,
);
/// Only populate episodes if the ID we started the load with is the
/// same as the one we have ended up with.
if (_podcast != null && lastFeed.podcast.url == _podcast!.url) {
_episodes = _podcast!.episodes;
if (_podcast!.newEpisodes) {
log.fine('We have new episodes to display');
_backgroundLoadStream.sink.add(BlocPopulatedState<void>());
_podcastStream.sink.add(BlocPopulatedState<Podcast>(results: _podcast));
} else if (_podcast!.updatedEpisodes) {
log.fine('We have updated episodes to re-display');
_refresh();
}
}
log.fine('Background loading successful state');
_backgroundLoadStream.sink.add(BlocSuccessfulState<void>());
}
Future<void> _loadFilteredEpisodes() async {
if (_podcast != null) {
_podcast = await podcastService.loadPodcast(
podcast: _podcast!,
highlightNewEpisodes: false,
refresh: false,
);
_episodes = _podcast!.episodes;
_podcastStream.add(BlocPopulatedState<Podcast>(results: _podcast));
_refresh();
}
}
/// Sets up a listener to handle requests to download an episode.
void _listenDownloadRequest() {
_downloadEpisode.listen((Episode? e) async {
log.fine('Received download request for ${e!.title}');
// To prevent a pause between the user tapping the download icon and
// the UI showing some sort of progress, set it to queued now.
var episode = _episodes.firstWhereOrNull((ep) => ep.guid == e.guid);
if (episode != null) {
episode.downloadState = e.downloadState = DownloadState.queued;
_refresh();
var result = await downloadService.downloadEpisode(e);
// If there was an error downloading the episode, push an error state
// and then restore to none.
if (!result) {
episode.downloadState = e.downloadState = DownloadState.failed;
_refresh();
episode.downloadState = e.downloadState = DownloadState.none;
_refresh();
}
}
});
}
/// Sets up a listener to listen for status updates from any currently downloading episode.
///
/// If the ID of a current download matches that of an episode currently in
/// use, we update the status of the episode and push it back into the episode stream.
void _listenDownloads() {
// Listen to download progress
MobileDownloadService.downloadProgress.listen((downloadProgress) {
downloadService.findEpisodeByTaskId(downloadProgress.id).then((downloadable) {
if (downloadable != null) {
// If the download matches a current episode push the update back into the stream.
var episode = _episodes.firstWhereOrNull((e) => e.downloadTaskId == downloadProgress.id);
if (episode != null) {
// Update the stream.
_refresh();
}
} else {
log.severe('Downloadable not found with id ${downloadProgress.id}');
}
});
});
}
/// Listen to episode change events sent by the [Repository]
void _listenEpisodeRepositoryEvents() {
podcastService.episodeListener!.listen((state) {
// Do we have this episode?
var eidx = _episodes.indexWhere((e) => e.guid == state.episode.guid && e.pguid == state.episode.pguid);
if (eidx != -1) {
_episodes[eidx] = state.episode;
_refresh();
}
});
}
// TODO: This needs refactoring to simplify the long switch statement.
void _listenPodcastStateEvents() async {
_podcastEvent.listen((event) async {
switch (event) {
case PodcastEvent.subscribe:
if (_podcast != null) {
// Emit loading state for subscription
_podcastStream.add(BlocLoadingState<Podcast>(_podcast));
// First, subscribe locally
_podcast = await podcastService.subscribe(_podcast!);
// Check if we're in a PinePods environment and also add to server
if (_podcast != null) {
try {
final settings = settingsService.settings;
if (settings != null &&
settings.pinepodsServer != null &&
settings.pinepodsApiKey != null &&
settings.pinepodsUserId != null) {
// Also add to PinePods server
final pinepodsService = PinepodsService();
pinepodsService.setCredentials(settings.pinepodsServer!, settings.pinepodsApiKey!);
final unifiedPodcast = UnifiedPinepodsPodcast(
id: 0,
indexId: 0,
title: _podcast!.title,
url: _podcast!.url ?? '',
originalUrl: _podcast!.url ?? '',
link: _podcast!.link ?? '',
description: _podcast!.description ?? '',
author: _podcast!.copyright ?? '',
ownerName: _podcast!.copyright ?? '',
image: _podcast!.imageUrl ?? '',
artwork: _podcast!.imageUrl ?? '',
lastUpdateTime: 0,
explicit: false,
episodeCount: 0,
);
await pinepodsService.addPodcast(unifiedPodcast, settings.pinepodsUserId!);
log.fine('Added podcast to PinePods server');
}
} catch (e) {
log.warning('Failed to add podcast to PinePods server: $e');
// Continue with local subscription even if server add fails
}
_episodes = _podcast!.episodes;
_podcastStream.add(BlocPopulatedState<Podcast>(results: _podcast));
_loadSubscriptions();
_refresh(); // Use _refresh to apply filters and update episode stream properly
}
}
break;
case PodcastEvent.unsubscribe:
if (_podcast != null) {
await podcastService.unsubscribe(_podcast!);
_podcast!.id = null;
_episodes = _podcast!.episodes;
_podcastStream.add(BlocPopulatedState<Podcast>(results: _podcast));
_loadSubscriptions();
_refresh(); // Use _refresh to apply filters and update episode stream properly
}
break;
case PodcastEvent.markAllPlayed:
if (_podcast != null && _podcast?.episodes != null) {
final changedEpisodes = <Episode>[];
for (var e in _podcast!.episodes) {
if (!e.played) {
e.played = true;
e.position = 0;
changedEpisodes.add(e);
}
}
await podcastService.saveEpisodes(changedEpisodes);
_episodesStream.add(_podcast!.episodes);
}
break;
case PodcastEvent.clearAllPlayed:
if (_podcast != null && _podcast?.episodes != null) {
final changedEpisodes = <Episode>[];
for (var e in _podcast!.episodes) {
if (e.played) {
e.played = false;
e.position = 0;
changedEpisodes.add(e);
}
}
await podcastService.saveEpisodes(changedEpisodes);
_episodesStream.add(_podcast!.episodes);
}
break;
case PodcastEvent.reloadSubscriptions:
_loadSubscriptions();
break;
case PodcastEvent.refresh:
_refresh();
break;
case PodcastEvent.episodeFilterNone:
if (_podcast != null) {
_podcast!.filter = PodcastEpisodeFilter.none;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
}
break;
case PodcastEvent.episodeFilterStarted:
_podcast!.filter = PodcastEpisodeFilter.started;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeFilterFinished:
_podcast!.filter = PodcastEpisodeFilter.played;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeFilterNotFinished:
_podcast!.filter = PodcastEpisodeFilter.notPlayed;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeSortDefault:
_podcast!.sort = PodcastEpisodeSort.none;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeSortLatest:
_podcast!.sort = PodcastEpisodeSort.latestFirst;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeSortEarliest:
_podcast!.sort = PodcastEpisodeSort.earliestFirst;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeSortAlphabeticalAscending:
_podcast!.sort = PodcastEpisodeSort.alphabeticalAscending;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
case PodcastEvent.episodeSortAlphabeticalDescending:
_podcast!.sort = PodcastEpisodeSort.alphabeticalDescending;
_podcast = await podcastService.save(_podcast!, withEpisodes: false);
await _loadFilteredEpisodes();
break;
}
});
}
void _listenPodcastSearchEvents() {
_podcastSearchEvent.debounceTime(const Duration(milliseconds: 200)).listen((search) {
_searchTerm = search;
applySearchFilter();
});
}
void applySearchFilter() {
if (_searchTerm.isEmpty) {
_episodesStream.add(_episodes);
} else {
var searchFilteredEpisodes =
_episodes.where((e) => e.title!.toLowerCase().contains(_searchTerm.trim().toLowerCase())).toList();
_episodesStream.add(searchFilteredEpisodes);
}
}
@override
void detach() {
downloadService.dispose();
}
@override
void dispose() {
_podcastFeed.close();
_downloadEpisode.close();
_subscriptions.close();
_podcastStream.close();
_episodesStream.close();
_podcastEvent.close();
MobileDownloadService.downloadProgress.close();
downloadService.dispose();
super.dispose();
}
/// Sink to load a podcast.
void Function(Feed) get load => _podcastFeed.add;
/// Sink to trigger an episode download.
void Function(Episode?) get downloadEpisode => _downloadEpisode.add;
void Function(PodcastEvent) get podcastEvent => _podcastEvent.add;
void Function(String) get podcastSearchEvent => _podcastSearchEvent.add;
/// Stream containing the current state of the podcast load.
Stream<BlocState<Podcast>> get details => _podcastStream.stream;
Stream<BlocState<void>> get backgroundLoading => _backgroundLoadStream.stream;
/// Stream containing the current list of Podcast episodes.
Stream<List<Episode?>?> get episodes => _episodesStream;
/// Obtain a list of podcast currently subscribed to.
Stream<List<Podcast>> get subscriptions => _subscriptions.stream;
}

View File

@@ -0,0 +1,63 @@
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:pinepods_mobile/bloc/bloc.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/services/podcast/podcast_service.dart';
import 'package:pinepods_mobile/state/queue_event_state.dart';
import 'package:rxdart/rxdart.dart';
/// Handles interaction with the Queue via an [AudioPlayerService].
class QueueBloc extends Bloc {
final AudioPlayerService audioPlayerService;
final PodcastService podcastService;
final PublishSubject<QueueEvent> _queueEvent = PublishSubject<QueueEvent>();
QueueBloc({
required this.audioPlayerService,
required this.podcastService,
}) {
_handleQueueEvents();
}
void _handleQueueEvents() {
_queueEvent.listen((QueueEvent event) async {
if (event is QueueAddEvent) {
var e = event.episode;
if (e != null) {
await audioPlayerService.addUpNextEpisode(e);
}
} else if (event is QueueRemoveEvent) {
var e = event.episode;
if (e != null) {
await audioPlayerService.removeUpNextEpisode(e);
}
} else if (event is QueueMoveEvent) {
var e = event.episode;
if (e != null) {
await audioPlayerService.moveUpNextEpisode(e, event.oldIndex, event.newIndex);
}
} else if (event is QueueClearEvent) {
await audioPlayerService.clearUpNext();
}
});
audioPlayerService.queueState!.debounceTime(const Duration(seconds: 2)).listen((event) {
podcastService.saveQueue(event.queue).then((value) {
/// Queue saved.
});
});
}
Function(QueueEvent) get queueEvent => _queueEvent.sink.add;
Stream<QueueListState>? get queue => audioPlayerService.queueState;
@override
void dispose() {
_queueEvent.close();
super.dispose();
}
}