From c11d986c3420b1e985903938381d51b3d48d4024 Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Fri, 8 Dec 2023 16:18:23 +0100 Subject: [PATCH] feature: download podcasts Fixes #240 --- lib/constants.dart | 1 + lib/src/app/master_items.dart | 26 ++--- lib/src/common/audio_page_body.dart | 8 +- lib/src/data/audio.dart | 5 +- lib/src/library/library_model.dart | 8 ++ lib/src/library/library_service.dart | 18 +++ lib/src/podcasts/download_button.dart | 56 +++++++++ lib/src/podcasts/download_model.dart | 66 +++++++++++ lib/src/podcasts/podcast_audio_tile.dart | 108 +++++++++--------- lib/src/podcasts/podcast_page.dart | 1 + .../podcasts/podcasts_collection_body.dart | 1 + lib/utils.dart | 57 +++++++++ pubspec.lock | 6 +- pubspec.yaml | 1 + 14 files changed, 291 insertions(+), 71 deletions(-) create mode 100644 lib/src/podcasts/download_button.dart create mode 100644 lib/src/podcasts/download_model.dart diff --git a/lib/constants.dart b/lib/constants.dart index ec505c34..4d82b740 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -93,6 +93,7 @@ const kStarredStationsFileName = 'starredStations.json'; const kSettingsFileName = 'settings.json'; const kLastPositionsFileName = 'lastPositions.json'; const kLocalAudioCacheFileName = 'localaudiocache.json'; +const kDownloads = 'downloads.json'; const kLocalAudioCache = 'localAudioCache'; const kUseLocalAudioCache = 'cacheSuggestionDisposed'; const kCreateCacheLimit = 1000; diff --git a/lib/src/app/master_items.dart b/lib/src/app/master_items.dart index 1207ec76..5ce7b280 100644 --- a/lib/src/app/master_items.dart +++ b/lib/src/app/master_items.dart @@ -112,20 +112,18 @@ List createMasterItems({ podcast.value.firstOrNull?.title ?? podcast.value.firstOrNull.toString(), ), - pageBuilder: (context) => isOnline - ? PodcastPage( - pageId: podcast.key, - title: podcast.value.firstOrNull?.album ?? - podcast.value.firstOrNull?.title ?? - podcast.value.firstOrNull.toString(), - audios: podcast.value, - onTextTap: onTextTap, - addPodcast: addPodcast, - removePodcast: removePodcast, - imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? - podcast.value.firstOrNull?.imageUrl, - ) - : const OfflinePage(), + pageBuilder: (context) => PodcastPage( + pageId: podcast.key, + title: podcast.value.firstOrNull?.album ?? + podcast.value.firstOrNull?.title ?? + podcast.value.firstOrNull.toString(), + audios: podcast.value, + onTextTap: onTextTap, + addPodcast: addPodcast, + removePodcast: removePodcast, + imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? + podcast.value.firstOrNull?.imageUrl, + ), iconBuilder: (context, selected) => PodcastPage.createIcon( context: context, imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? diff --git a/lib/src/common/audio_page_body.dart b/lib/src/common/audio_page_body.dart index 9ccce812..903aa7e1 100644 --- a/lib/src/common/audio_page_body.dart +++ b/lib/src/common/audio_page_body.dart @@ -7,6 +7,7 @@ import '../../common.dart'; import '../../data.dart'; import '../../player.dart'; import '../../podcasts.dart'; +import '../app/connectivity_notifier.dart'; import '../library/library_model.dart'; class AudioPageBody extends StatefulWidget { @@ -90,6 +91,7 @@ class _AudioPageBodyState extends State { @override Widget build(BuildContext context) { + final isOnline = context.select((ConnectivityNotifier c) => c.isOnline); final isPlaying = context.select((PlayerModel m) => m.isPlaying); final playerModel = context.read(); @@ -209,13 +211,16 @@ class _AudioPageBodyState extends State { List.generate(sortedAudios.take(_amount).length, (index) { final audio = sortedAudios.elementAt(index); final audioSelected = currentAudio == audio; + final download = libraryModel.getDownload(audio.url); if (audio.audioType == AudioType.podcast) { return PodcastAudioTile( removeUpdate: () => libraryModel.removePodcastUpdate(widget.pageId), isExpanded: audioSelected, - audio: audio, + audio: download != null + ? audio.copyWith(path: download) + : audio, isPlayerPlaying: isPlaying, selected: audioSelected, pause: pause, @@ -226,6 +231,7 @@ class _AudioPageBodyState extends State { play: play, lastPosition: libraryModel.getLastPosition.call(audio.url), safeLastPosition: playerModel.safeLastPosition, + isOnline: isOnline, ); } diff --git a/lib/src/data/audio.dart b/lib/src/data/audio.dart index 981642a0..a424e329 100644 --- a/lib/src/data/audio.dart +++ b/lib/src/data/audio.dart @@ -252,8 +252,9 @@ class Audio { if (identical(this, other)) return true; return other is Audio && - other.path == path && - other.url == url && + (other.path == path || + (other.url == url && other.path != null) || + other.url == url) && other.audioType == audioType && other.imageUrl == imageUrl && other.description == description && diff --git a/lib/src/library/library_model.dart b/lib/src/library/library_model.dart index 7b5c8b65..9e4de257 100644 --- a/lib/src/library/library_model.dart +++ b/lib/src/library/library_model.dart @@ -22,6 +22,7 @@ class LibraryModel extends SafeChangeNotifier { StreamSubscription? _neverShowFailedImportsSub; StreamSubscription? _favTagsSub; StreamSubscription? _lastFavSub; + StreamSubscription? _downloadsSub; bool get neverShowFailedImports => _service.neverShowFailedImports; Future setNeverShowLocalImports() async => @@ -56,6 +57,7 @@ class LibraryModel extends SafeChangeNotifier { _service.neverShowFailedImportsChanged.listen((_) => notifyListeners()); _favTagsSub = _service.favTagsChanged.listen((_) => notifyListeners()); _lastFavSub = _service.lastFavChanged.listen((_) => notifyListeners()); + _downloadsSub = _service.downloadsChanged.listen((_) => notifyListeners()); notifyListeners(); } @@ -79,6 +81,7 @@ class LibraryModel extends SafeChangeNotifier { _neverShowFailedImportsSub?.cancel(); _favTagsSub?.cancel(); _lastFavSub?.cancel(); + _downloadsSub?.cancel(); super.dispose(); } @@ -184,6 +187,11 @@ class LibraryModel extends SafeChangeNotifier { void removePodcastUpdate(String feedUrl) => _service.removePodcastUpdate(feedUrl); + int get downloadsLength => _service.downloads.length; + + String? getDownload(String? url) => + url == null ? null : _service.downloads[url]; + // // Albums // diff --git a/lib/src/library/library_service.dart b/lib/src/library/library_service.dart index 2f4b4544..1b601243 100644 --- a/lib/src/library/library_service.dart +++ b/lib/src/library/library_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../../constants.dart'; @@ -227,7 +228,21 @@ class LibraryService { } // Podcasts + final dio = Dio(); + Map _downloads = {}; + Map get downloads => _downloads; + String? getDownload(String? url) => downloads[url]; + final _downloadsController = StreamController.broadcast(); + Stream get downloadsChanged => _downloadsController.stream; + void addDownload(String url, String path) { + _downloads.putIfAbsent(url, () => path); + writeStringMap(_downloads, kDownloads) + .then((_) => _downloadsController.add(true)); + } + + String? _downloadsDir; + String? get downloadsDir => _downloadsDir; Map> _podcasts = {}; Map> get podcasts => _podcasts; int get podcastsLength => _podcasts.length; @@ -342,6 +357,8 @@ class LibraryService { (await readAudioMap(kLikedAudios)).entries.firstOrNull?.value ??