diff --git a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift index 42432ac3ec..ae5243adbf 100644 --- a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift +++ b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift @@ -299,9 +299,10 @@ public enum PlayerAction: String, Codable, Equatable { case archive = "archive" case addBookmark = "bookmark" case transcript = "transcript" + case download = "download" } -extension Array: RawRepresentable where Element: RawRepresentable { +extension Array: @retroactive RawRepresentable where Element: RawRepresentable { public typealias RawValue = String public init?(rawValue: String) { diff --git a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Model/BaseEpisode.swift b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Model/BaseEpisode.swift index c736583d26..eda6d3f7fd 100644 --- a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Model/BaseEpisode.swift +++ b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Model/BaseEpisode.swift @@ -67,3 +67,9 @@ import Foundation /// Whether this is a regular episode, or an user episode (File) var isUserEpisode: Bool { get } } + +extension BaseEpisode { + public var isInDownloadProcess: Bool { + return downloading() || queued() || waitingForWifi() + } +} diff --git a/PocketCastsTests/Tests/Utilities/SettingsTests.swift b/PocketCastsTests/Tests/Utilities/SettingsTests.swift index 0cf1ee8103..053faa972a 100644 --- a/PocketCastsTests/Tests/Utilities/SettingsTests.swift +++ b/PocketCastsTests/Tests/Utilities/SettingsTests.swift @@ -65,6 +65,7 @@ final class SettingsTests: XCTestCase { .effects, .sleepTimer, .routePicker, + .download, .starEpisode, .shareEpisode, .goToPodcast, @@ -86,6 +87,7 @@ final class SettingsTests: XCTestCase { .effects, .sleepTimer, .routePicker, + .download, .starEpisode, .shareEpisode, .goToPodcast, @@ -113,6 +115,7 @@ final class SettingsTests: XCTestCase { .effects, .sleepTimer, .routePicker, + .download, .starEpisode, .shareEpisode, .goToPodcast, diff --git a/config/Version.xcconfig b/config/Version.xcconfig index a7caa43e6e..c4c020cc53 100644 --- a/config/Version.xcconfig +++ b/config/Version.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 7.76.0.0 +VERSION_LONG = 7.76.0.1 VERSION_SHORT = 7.76 diff --git a/fastlane/Frozen.strings b/fastlane/Frozen.strings index 81a0ec0777..faaceed905 100644 --- a/fastlane/Frozen.strings +++ b/fastlane/Frozen.strings @@ -4521,6 +4521,15 @@ /* Auto Downloads Setting - Limits downloads to a number of show episodes. `%1$@' is a placeholder for the number of episodes*/ "auto_download_limit_number_of_episodes_show" = "%1$@ Latest Episodes per Show"; +/* Toast message when episode download is removed*/ +"player_episode_was_removed" = "Episode was removed"; + +/* Toast message when episode is queued for download*/ +"player_episode_queued_for_download" = "Episode queued for download"; + +/* Toast message when episode download is cancelled*/ +"player_episode_download_cancelled" = "Episode download cancelled"; + /* MARK: - InfoPlist.strings */ diff --git a/podcasts/EffectsViewController.swift b/podcasts/EffectsViewController.swift index 614403c486..0c86e7f6f8 100644 --- a/podcasts/EffectsViewController.swift +++ b/podcasts/EffectsViewController.swift @@ -189,6 +189,7 @@ class EffectsViewController: SimpleNotificationsViewController { updateControls() if FeatureFlag.customPlaybackSettings.enabled { + PlaybackManager.shared.updateIfPodcastUsedCustomEffectsBefore() playbackSettingsSegmentedControl.selectedSegmentIndex = PlaybackManager.shared.isCurrentEffectGlobal() ? 0 : 1 } if let episode = PlaybackManager.shared.currentEpisode() as? Episode, let podcast = episode.parentPodcast() { diff --git a/podcasts/Enumerations.swift b/podcasts/Enumerations.swift index 8a1baa0178..e474f72823 100644 --- a/podcasts/Enumerations.swift +++ b/podcasts/Enumerations.swift @@ -245,7 +245,7 @@ extension PlayerAction: AnalyticsDescribable { /// Specify default actions and their order static var defaultActions: [PlayerAction] { [ - .effects, .sleepTimer, .routePicker, .transcript, + .effects, .sleepTimer, .routePicker, .transcript, .download, .starEpisode, .shareEpisode, .goToPodcast, .chromecast, .markPlayed, .addBookmark, .archive ] @@ -275,6 +275,8 @@ extension PlayerAction: AnalyticsDescribable { self = .addBookmark case 11: self = .transcript + case 12: + self = .download default: return nil } @@ -304,6 +306,8 @@ extension PlayerAction: AnalyticsDescribable { return 10 case .transcript: return 11 + case .download: + return 12 } } @@ -345,6 +349,11 @@ extension PlayerAction: AnalyticsDescribable { return L10n.addBookmark case .transcript: return L10n.transcript + case .download: + guard let episode else { + return L10n.download + } + return episode.downloaded(pathFinder: DownloadManager.shared) ? L10n.removeDownload : (episode.isInDownloadProcess ? L10n.statusDownloading : L10n.download) } } @@ -383,6 +392,11 @@ extension PlayerAction: AnalyticsDescribable { return "bookmarks-shelf-overflow-icon" case .transcript: return "transcript" + case .download: + guard let episode else { + return "episode-download" + } + return episode.downloaded(pathFinder: DownloadManager.shared) ? "episode-downloaded" : "episode-download" } } @@ -410,6 +424,11 @@ extension PlayerAction: AnalyticsDescribable { return "bookmarks-shelf-icon" case .transcript: return "transcript" + case .download: + guard let episode else { + return "episode-download" + } + return episode.downloaded(pathFinder: DownloadManager.shared) ? "episode-downloaded" : "episode-download" } } @@ -459,6 +478,8 @@ extension PlayerAction: AnalyticsDescribable { return "bookmark" case .transcript: return "transcript" + case .download: + return "download" } } } diff --git a/podcasts/NowPlayingPlayerItemViewController+Shelf.swift b/podcasts/NowPlayingPlayerItemViewController+Shelf.swift index 2005f250e7..2b040765eb 100644 --- a/podcasts/NowPlayingPlayerItemViewController+Shelf.swift +++ b/podcasts/NowPlayingPlayerItemViewController+Shelf.swift @@ -16,18 +16,19 @@ protocol NowPlayingActionsDelegate: AnyObject { func archiveTapped() func bookmarkTapped() func transcriptTapped() - + func downloadTapped() func sharedRoutePicker(largeSize: Bool) -> PCRoutePickerView } extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate { + @objc func reloadShelfActions() { guard let playingEpisode = PlaybackManager.shared.currentEpisode() else { return } let actions = Settings.playerActions() // don't reload the actions unless we need to - if !lastShelfLoadState.updateRequired(shelfActions: actions, episodeUuid: playingEpisode.uuid, effectsOn: PlaybackManager.shared.effects().effectsEnabled(), sleepTimerOn: PlaybackManager.shared.sleepTimerActive(), episodeStarred: playingEpisode.keepEpisode) { return } + if !lastShelfLoadState.updateRequired(shelfActions: actions, episodeUuid: playingEpisode.uuid, effectsOn: PlaybackManager.shared.effects().effectsEnabled(), sleepTimerOn: PlaybackManager.shared.sleepTimerActive(), episodeStarred: playingEpisode.keepEpisode, episodeStatus: playingEpisode.episodeStatus) { return } // load the first 4 actions into the player, followed by an overflow icon playerControlsStackView.removeAllSubviews() @@ -151,6 +152,16 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate { button.addTarget(self, action: #selector(transcriptTapped(_:)), for: .touchUpInside) button.accessibilityLabel = L10n.transcript + addToShelf(on: button) + + case .download: + let button = UIButton(frame: CGRect.zero) + button.isPointerInteractionEnabled = true + button.imageView?.tintColor = ThemeColor.playerContrast02() + button.setImage(UIImage(named: action.largeIconName(episode: playingEpisode)), for: .normal) + button.addTarget(self, action: #selector(downloadTapped(_:)), for: .touchUpInside) + button.accessibilityLabel = L10n.download + addToShelf(on: button) } @@ -234,6 +245,41 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate { displayTranscript = true } + func downloadTapped() { + guard let episode = PlaybackManager.shared.currentEpisode() as? Episode else { return } + + AnalyticsEpisodeHelper.shared.currentSource = analyticsSource + + if episode.downloaded(pathFinder: DownloadManager.shared) { + let confirmation = OptionsPicker(title: L10n.podcastDetailsRemoveDownload) + let yesAction = OptionAction(label: L10n.remove, icon: nil) { + self.deleteDownloadedFile() + Toast.show(L10n.playerEpisodeWasRemoved) + } + yesAction.destructive = true + confirmation.addAction(action: yesAction) + + confirmation.show(statusBarStyle: preferredStatusBarStyle) + } else if episode.isInDownloadProcess { + PlaybackActionHelper.stopDownload(episodeUuid: episode.uuid) + Toast.show(L10n.playerEpisodeDownloadCancelled) + } else { + PlaybackActionHelper.download(episodeUuid: episode.uuid) + Toast.show(L10n.playerEpisodeQueuedForDownload) + } + } + + private func deleteDownloadedFile() { + guard let episode = PlaybackManager.shared.currentEpisode() as? Episode else { return } + + EpisodeManager.analyticsHelper.currentSource = analyticsSource + + PlaybackManager.shared.removeIfPlayingOrQueued(episode: episode, fireNotification: true, userInitiated: false) + EpisodeManager.deleteDownloadedFiles(episode: episode, userInitated: true) + + NotificationCenter.postOnMainThread(notification: Constants.Notifications.episodeDownloadStatusChanged, object: episode.uuid) + } + // MARK: - Player Actions private func presentUsingSheet(_ viewController: UIViewController, forceLarge: Bool = false) { if let sheetController = viewController.sheetPresentationController { @@ -319,6 +365,11 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate { displayTranscript = true } + @objc private func downloadTapped(_ sender: UIButton) { + shelfButtonTapped(.download) + downloadTapped() + } + // MARK: - Sleep Timer @objc func sleepTimerUpdated() { diff --git a/podcasts/NowPlayingPlayerItemViewController+Update.swift b/podcasts/NowPlayingPlayerItemViewController+Update.swift index 432b3f38b8..5b6ca290f7 100644 --- a/podcasts/NowPlayingPlayerItemViewController+Update.swift +++ b/podcasts/NowPlayingPlayerItemViewController+Update.swift @@ -20,6 +20,7 @@ extension NowPlayingPlayerItemViewController { addCustomObserver(Constants.Notifications.playerActionsUpdated, selector: #selector(reloadShelfActions)) addCustomObserver(UIApplication.willEnterForegroundNotification, selector: #selector(update)) addCustomObserver(Constants.Notifications.episodeStarredChanged, selector: #selector(reloadShelfActions)) + addCustomObserver(Constants.Notifications.episodeDownloadStatusChanged, selector: #selector(reloadShelfActions)) } @objc private func playbackTrackChanged() { diff --git a/podcasts/PlaybackManager.swift b/podcasts/PlaybackManager.swift index 08ec6b8a29..81db9454fe 100644 --- a/podcasts/PlaybackManager.swift +++ b/podcasts/PlaybackManager.swift @@ -870,14 +870,25 @@ class PlaybackManager: ServerPlaybackDelegate { let podcast = episode.parentPodcast() else { return } + overrideEffectsToggled(applyLocalSettings: applyLocalSettings, for: podcast) + } + + func overrideEffectsToggled(applyLocalSettings: Bool, for podcast: Podcast) { podcast.isEffectsOverridden = applyLocalSettings DataManager.sharedManager.save(podcast: podcast) NotificationCenter.postOnMainThread(notification: Constants.Notifications.podcastUpdated, object: podcast.uuid) - let newEffects = loadEffects() - currentEffects = newEffects - handlePlaybackEffectsChanged(effects: newEffects) + effectsChangedExternally() + } + + func updateIfPodcastUsedCustomEffectsBefore() { + if let episode = currentEpisode() as? Episode, let podcast = episode.parentPodcast() { + if podcast.overrideGlobalEffects, !podcast.usedCustomEffectsBefore { + podcast.usedCustomEffectsBefore = true + DataManager.sharedManager.save(podcast: podcast) + } + } } func isCurrentEffectGlobal() -> Bool { diff --git a/podcasts/PodcastEffectsViewController+Table.swift b/podcasts/PodcastEffectsViewController+Table.swift index 001e4d8b7e..a3abe92212 100644 --- a/podcasts/PodcastEffectsViewController+Table.swift +++ b/podcasts/PodcastEffectsViewController+Table.swift @@ -45,7 +45,11 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega case .playbackSpeed: let cell = tableView.dequeueReusableCell(withIdentifier: PodcastEffectsViewController.timeStepperCellId, for: indexPath) as! TimeStepperCell cell.cellLabel?.text = L10n.settingsPlaySpeed - if FeatureFlag.newSettingsStorage.enabled { + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + cell.cellSecondaryLabel.text = L10n.playbackSpeed(effect.playbackSpeed.localized()) + } else if FeatureFlag.newSettingsStorage.enabled { cell.cellSecondaryLabel.text = L10n.playbackSpeed(podcast.settings.playbackSpeed.localized()) } else { cell.cellSecondaryLabel.text = L10n.playbackSpeed(podcast.playbackSpeed.localized()) @@ -57,7 +61,12 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega cell.timeStepper.maximumValue = 5 cell.timeStepper.smallIncrements = 0.1 cell.timeStepper.smallIncrementThreshold = TimeInterval.greatestFiniteMagnitude - if FeatureFlag.newSettingsStorage.enabled { + + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + cell.timeStepper.currentValue = effect.playbackSpeed + } else if FeatureFlag.newSettingsStorage.enabled { cell.timeStepper.currentValue = podcast.settings.playbackSpeed } else { cell.timeStepper.currentValue = podcast.playbackSpeed @@ -73,7 +82,11 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega cell.cellLabel?.text = L10n.settingsTrimSilence cell.cellSwitch.onTintColor = podcast.switchTintColor() cell.setImage(imageName: "player_trim") - if FeatureFlag.newSettingsStorage.enabled { + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + cell.cellSwitch.isOn = effect.trimSilence != .off + } else if FeatureFlag.newSettingsStorage.enabled { cell.cellSwitch.isOn = podcast.settings.trimSilence != .off } else { cell.cellSwitch.isOn = podcast.trimSilenceAmount > 0 @@ -90,7 +103,11 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega let trimAmount: TrimSilenceAmount - if FeatureFlag.newSettingsStorage.enabled { + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + trimAmount = effect.trimSilence + } else if FeatureFlag.newSettingsStorage.enabled { trimAmount = podcast.settings.trimSilence.amount } else { trimAmount = TrimSilenceAmount(rawValue: Int32(podcast.trimSilenceAmount)) ?? .low @@ -103,7 +120,11 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega cell.cellLabel?.text = L10n.settingsVolumeBoost cell.cellSwitch.onTintColor = podcast.switchTintColor() cell.setImage(imageName: "player_volumeboost") - if FeatureFlag.newSettingsStorage.enabled { + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + cell.cellSwitch.isOn = effect.volumeBoost + } else if FeatureFlag.newSettingsStorage.enabled { cell.cellSwitch.isOn = podcast.settings.boostVolume } else { cell.cellSwitch.isOn = podcast.boostVolume @@ -218,18 +239,27 @@ extension PodcastEffectsViewController: UITableViewDataSource, UITableViewDelega } @objc private func overrideEffectsToggled(_ sender: UISwitch) { - podcast.isEffectsOverridden = sender.isOn - podcast.syncStatus = SyncStatus.notSynced.rawValue - if FeatureFlag.customPlaybackSettings.enabled && !podcast.usedCustomEffectsBefore { - podcast.usedCustomEffectsBefore = true + if FeatureFlag.customPlaybackSettings.enabled { + PlaybackManager.shared.overrideEffectsToggled(applyLocalSettings: sender.isOn, for: podcast) + effectsTable.reloadData() + } else { + podcast.isEffectsOverridden = sender.isOn + podcast.syncStatus = SyncStatus.notSynced.rawValue + saveUpdates() } - saveUpdates() Analytics.track(.podcastSettingsCustomPlaybackEffectsToggled, properties: ["enabled": sender.isOn]) } private func tableData() -> [[TableRow]] { - let hasTrimSilence = FeatureFlag.newSettingsStorage.enabled ? podcast.settings.trimSilence != .off : podcast.trimSilenceAmount > 0 + let hasTrimSilence: Bool + if FeatureFlag.customPlaybackSettings.enabled, + !podcast.usedCustomEffectsBefore { + let effect = PlaybackManager.shared.effects() + hasTrimSilence = effect.trimSilence != .off + } else { + hasTrimSilence = FeatureFlag.newSettingsStorage.enabled ? podcast.settings.trimSilence != .off : podcast.trimSilenceAmount > 0 + } if podcast.isEffectsOverridden && hasTrimSilence { return [[.customForPodcast], [.playbackSpeed, .trimSilence, .trimSilenceAmount, .volumeBoost]] } diff --git a/podcasts/ShelfActionsViewController+Table.swift b/podcasts/ShelfActionsViewController+Table.swift index 6cb9073ba1..72573efd18 100644 --- a/podcasts/ShelfActionsViewController+Table.swift +++ b/podcasts/ShelfActionsViewController+Table.swift @@ -116,7 +116,8 @@ extension ShelfActionsViewController: UITableViewDelegate, UITableViewDataSource self.playerActionsDelegate?.bookmarkTapped() case .transcript: self.playerActionsDelegate?.transcriptTapped() - return + case.download: + self.playerActionsDelegate?.downloadTapped() } } } diff --git a/podcasts/ShelfLoadState.swift b/podcasts/ShelfLoadState.swift index abcfd9ebf8..1f2645e256 100644 --- a/podcasts/ShelfLoadState.swift +++ b/podcasts/ShelfLoadState.swift @@ -7,9 +7,10 @@ struct ShelfLoadState { private var effectsAreOn = false private var sleepTimerIsOn = false private var episodeIsStarred = false + private var episodeStatus: Int32 = 0 - mutating func updateRequired(shelfActions: [PlayerAction], episodeUuid: String, effectsOn: Bool, sleepTimerOn: Bool, episodeStarred: Bool) -> Bool { - if lastShelfActionsLoaded == shelfActions, lastShelfEpisodeUuid == episodeUuid, effectsAreOn == effectsOn, sleepTimerIsOn == sleepTimerOn, episodeIsStarred == episodeStarred { + mutating func updateRequired(shelfActions: [PlayerAction], episodeUuid: String, effectsOn: Bool, sleepTimerOn: Bool, episodeStarred: Bool, episodeStatus: Int32) -> Bool { + if lastShelfActionsLoaded == shelfActions, lastShelfEpisodeUuid == episodeUuid, effectsAreOn == effectsOn, sleepTimerIsOn == sleepTimerOn, episodeIsStarred == episodeStarred, episodeStatus == self.episodeStatus { return false } @@ -18,6 +19,7 @@ struct ShelfLoadState { effectsAreOn = effectsOn sleepTimerIsOn = sleepTimerOn episodeIsStarred = episodeStarred + self.episodeStatus = episodeStatus return true } diff --git a/podcasts/Strings+Generated.swift b/podcasts/Strings+Generated.swift index df1b7d265e..766509e9e2 100644 --- a/podcasts/Strings+Generated.swift +++ b/podcasts/Strings+Generated.swift @@ -1810,6 +1810,12 @@ internal enum L10n { internal static func playerEffectsTrimSilenceProgress(_ p1: Any) -> String { return L10n.tr("Localizable", "player_effects_trim_silence_progress", String(describing: p1)) } + /// Episode download cancelled + internal static var playerEpisodeDownloadCancelled: String { return L10n.tr("Localizable", "player_episode_download_cancelled") } + /// Episode queued for download + internal static var playerEpisodeQueuedForDownload: String { return L10n.tr("Localizable", "player_episode_queued_for_download") } + /// Episode was removed + internal static var playerEpisodeWasRemoved: String { return L10n.tr("Localizable", "player_episode_was_removed") } /// The episode might be corrupted, but you can try to play it again. internal static var playerErrorCorruptedFile: String { return L10n.tr("Localizable", "player_error_corrupted_file") } /// Check your Internet connection and try again. diff --git a/podcasts/en.lproj/Localizable.strings b/podcasts/en.lproj/Localizable.strings index 56ac896369..77e864b63f 100644 --- a/podcasts/en.lproj/Localizable.strings +++ b/podcasts/en.lproj/Localizable.strings @@ -4517,4 +4517,13 @@ /* Auto Downloads Setting - Limits downloads to a number of show episodes. `%1$@' is a placeholder for the number of episodes*/ "auto_download_limit_number_of_episodes_show" = "%1$@ Latest Episodes per Show"; +/* Toast message when episode download is removed*/ +"player_episode_was_removed" = "Episode was removed"; + +/* Toast message when episode is queued for download*/ +"player_episode_queued_for_download" = "Episode queued for download"; + +/* Toast message when episode download is cancelled*/ +"player_episode_download_cancelled" = "Episode download cancelled"; +