From f2804fb3db695a009e80b26eb235ed40fe6efee7 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Tue, 3 Dec 2024 11:36:34 -0700 Subject: [PATCH 1/4] Add sort options to Downloads --- .../PocketCastsDataModel/Public/Enums.swift | 4 ++ podcasts/Analytics/AnalyticsEvent.swift | 1 + podcasts/DownloadsViewController.swift | 45 ++++++++++++++++++- podcasts/EpisodesDataManager.swift | 25 +++++++++-- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift index ae5243adbf..2d5391e853 100644 --- a/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift +++ b/Modules/DataModel/Sources/PocketCastsDataModel/Public/Enums.swift @@ -67,6 +67,10 @@ public enum PlaylistSort: Int32 { case newestToOldest = 0, oldestToNewest = 1, shortestToLongest = 2, longestToShortest = 3 } +public enum DownloadsSort: Int32 { + case newestToOldest = 0, oldestToNewest = 1, largestToSmallest = 2, smallestToLargest = 3 +} + public struct EpisodeBasicData { public init() {} diff --git a/podcasts/Analytics/AnalyticsEvent.swift b/podcasts/Analytics/AnalyticsEvent.swift index 0abc4c1d7f..3594fa2a36 100644 --- a/podcasts/Analytics/AnalyticsEvent.swift +++ b/podcasts/Analytics/AnalyticsEvent.swift @@ -170,6 +170,7 @@ enum AnalyticsEvent: String { case downloadsMultiSelectEntered case downloadsSelectAllButtonTapped case downloadsMultiSelectExited + case downloadsSortByChanged // MARK: - Downloads Clean Up View diff --git a/podcasts/DownloadsViewController.swift b/podcasts/DownloadsViewController.swift index 1acc6d1859..b0d2b1eccf 100644 --- a/podcasts/DownloadsViewController.swift +++ b/podcasts/DownloadsViewController.swift @@ -207,7 +207,7 @@ class DownloadsViewController: PCViewController { operationQueue.addOperation { [weak self] in guard let self else { return } - let newData = self.episodesDataManager.downloadedEpisodes() + let newData = self.episodesDataManager.downloadedEpisodes(sort: sort) DispatchQueue.main.sync { self.downloadsTable.isHidden = (newData.count == 0) @@ -230,11 +230,39 @@ class DownloadsViewController: PCViewController { dismiss(animated: true, completion: nil) } + func showSortByPicker() { + let optionsPicker = OptionsPicker(title: L10n.sortBy.localizedUppercase) + + addSortAction(to: optionsPicker, sortOrder: .newestToOldest) + addSortAction(to: optionsPicker, sortOrder: .oldestToNewest) + addSortAction(to: optionsPicker, sortOrder: .largestToSmallest) + addSortAction(to: optionsPicker, sortOrder: .smallestToLargest) + + optionsPicker.show(statusBarStyle: AppTheme.defaultStatusBarStyle()) + } + + private var sort: DownloadsSort = .newestToOldest + + private func addSortAction(to optionPicker: OptionsPicker, sortOrder: DownloadsSort) { + let action = OptionAction(label: sortOrder.description, selected: sort == sortOrder) { + Analytics.track(.downloadsSortByChanged, properties: ["sort_order": sortOrder]) + self.sort = sortOrder + self.reloadEpisodes() + } + optionPicker.addAction(action: action) + } + @objc private func menuTapped(_ sender: UIBarButtonItem) { Analytics.track(.downloadsOptionsButtonTapped) let optionsPicker = OptionsPicker(title: nil) + let sortAction = OptionAction(label: L10n.sortBy, secondaryLabel: sort.description, icon: "podcastlist_sort") { [weak self] in + Analytics.track(.downloadsOptionsModalOptionTapped, properties: ["option": "sort_by"]) + self?.showSortByPicker() + } + optionsPicker.addAction(action: sortAction) + let MultiSelectAction = OptionAction(label: L10n.selectEpisodes, icon: "option-multiselect") { [weak self] in Analytics.track(.downloadsOptionsModalOptionTapped, properties: ["option": "select_episodes"]) self?.isMultiSelectEnabled = true @@ -336,3 +364,18 @@ extension DownloadsViewController: AnalyticsSourceProvider { .downloads } } + +public extension DownloadsSort { + var description: String { + switch self { + case .newestToOldest: + return "Newest to Oldest" + case .oldestToNewest: + return "Oldest to Newest" + case .largestToSmallest: + return "Largest to Smallest" + case .smallestToLargest: + return "Smallest to Largest" + } + } +} diff --git a/podcasts/EpisodesDataManager.swift b/podcasts/EpisodesDataManager.swift index e7cfbed9d4..e564a7f4de 100644 --- a/podcasts/EpisodesDataManager.swift +++ b/podcasts/EpisodesDataManager.swift @@ -17,7 +17,7 @@ class EpisodesDataManager { return episodes(for: filter).map { $0.episode } } case .downloads: - return downloadedEpisodes().flatMap { $0.elements.map { $0.episode } } + return downloadedEpisodes(sort: .newestToOldest).flatMap { $0.elements.map { $0.episode } } case .files: return uploadedEpisodes() case .starred: @@ -131,8 +131,27 @@ class EpisodesDataManager { // MARK: - Downloads - func downloadedEpisodes() -> [ArraySection] { - let query = "( ((downloadTaskId IS NOT NULL AND autoDownloadStatus <> \(AutoDownloadStatus.playerDownloadedForStreaming.rawValue) ) OR episodeStatus = \(DownloadStatus.downloaded.rawValue) OR episodeStatus = \(DownloadStatus.waitingForWifi.rawValue)) OR (episodeStatus = \(DownloadStatus.downloadFailed.rawValue) AND lastDownloadAttemptDate > ?) ) ORDER BY lastDownloadAttemptDate DESC LIMIT 1000" + func downloadedEpisodes(sort: DownloadsSort) -> [ArraySection] { + let orderByColumn: String + let orderByOrder: String + + switch sort { + case .newestToOldest: + orderByColumn = "lastDownloadAttemptDate" + orderByOrder = "DESC" + case .oldestToNewest: + orderByColumn = "lastDownloadAttemptDate" + orderByOrder = "ASC" + case .largestToSmallest: + orderByColumn = "sizeInBytes" + orderByOrder = "DESC" + case .smallestToLargest: + orderByColumn = "sizeInBytes" + orderByOrder = "ASC" + } + + let query = "( ((downloadTaskId IS NOT NULL AND autoDownloadStatus <> \(AutoDownloadStatus.playerDownloadedForStreaming.rawValue) ) OR episodeStatus = \(DownloadStatus.downloaded.rawValue) OR episodeStatus = \(DownloadStatus.waitingForWifi.rawValue)) OR (episodeStatus = \(DownloadStatus.downloadFailed.rawValue) AND lastDownloadAttemptDate > ?) ) ORDER BY \(orderByColumn) \(orderByOrder) LIMIT 1000" + let arguments = [Date().weeksAgo(1)] as [Any] let newData = EpisodeTableHelper.loadSectionedEpisodes(query: query, arguments: arguments, episodeShortKey: { episode -> String in From 5028a0ed395adab6cb293ce0a3fb62da5934e22a Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Tue, 3 Dec 2024 11:49:17 -0700 Subject: [PATCH 2/4] Add size to Downloads episode cells --- podcasts/DownloadsViewController+Table.swift | 1 + podcasts/EpisodeCell.swift | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/podcasts/DownloadsViewController+Table.swift b/podcasts/DownloadsViewController+Table.swift index e30c34a27d..41b1500607 100644 --- a/podcasts/DownloadsViewController+Table.swift +++ b/podcasts/DownloadsViewController+Table.swift @@ -53,6 +53,7 @@ extension DownloadsViewController: UITableViewDelegate, UITableViewDataSource { if let episode = episodeAtIndexPath(indexPath) { cell.populateFrom(episode: episode, tintColor: ThemeColor.primaryIcon01()) cell.shouldShowSelect = isMultiSelectEnabled + cell.showsFileSize = true if isMultiSelectEnabled { cell.showTick = selectedEpisodesContains(uuid: episode.uuid) } diff --git a/podcasts/EpisodeCell.swift b/podcasts/EpisodeCell.swift index 6fc7092fda..0798c65db9 100644 --- a/podcasts/EpisodeCell.swift +++ b/podcasts/EpisodeCell.swift @@ -79,6 +79,7 @@ class EpisodeCell: ThemeableSwipeCell, MainEpisodeActionViewDelegate { } var hidesArtwork = false + var showsFileSize = false var playlist: AutoplayHelper.Playlist? @@ -258,9 +259,9 @@ class EpisodeCell: ThemeableSwipeCell, MainEpisodeActionViewDelegate { if episode.archived { informationLabel.text = L10n.podcastArchived + " • " + episode.displayableInfo(includeSize: false) } else if let userEpisode = episode as? UserEpisode { - informationLabel.text = userEpisode.displayableInfo(includeSize: Settings.primaryRowAction() == .download) + informationLabel.text = userEpisode.displayableInfo(includeSize: Settings.primaryRowAction() == .download || showsFileSize) } else { - informationLabel.text = episode.displayableInfo(includeSize: Settings.primaryRowAction() == .download) + informationLabel.text = episode.displayableInfo(includeSize: Settings.primaryRowAction() == .download || showsFileSize) } if episode.downloading(), !downloadingIndicator.isAnimating { From a95a687715de833a585442fc88370e8b290bff91 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Tue, 3 Dec 2024 16:36:49 -0700 Subject: [PATCH 3/4] Add section headers --- podcasts/EpisodesDataManager.swift | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/podcasts/EpisodesDataManager.swift b/podcasts/EpisodesDataManager.swift index e564a7f4de..fbccb60a6c 100644 --- a/podcasts/EpisodesDataManager.swift +++ b/podcasts/EpisodesDataManager.swift @@ -155,12 +155,44 @@ class EpisodesDataManager { let arguments = [Date().weeksAgo(1)] as [Any] let newData = EpisodeTableHelper.loadSectionedEpisodes(query: query, arguments: arguments, episodeShortKey: { episode -> String in - episode.shortLastDownloadAttemptDate() + switch sort { + case .newestToOldest, .oldestToNewest: + episode.shortLastDownloadAttemptDate() + case .smallestToLargest, .largestToSmallest: + categorizeFileSize(sizeInBytes: episode.sizeInBytes) + } }) return newData } + private func categorizeFileSize(sizeInBytes: Int64) -> String { + switch sizeInBytes { + case 0..<1_000_000: // Less than 1MB + return "< \(formattedFileSize(sizeInBytes: 1_000_000))" + case 1_000_000..<10_000_000: // 1MB to < 10MB + return "\(formattedFileSize(sizeInBytes: 1_000_000))-\(formattedFileSize(sizeInBytes: 10_000_000))" + case 10_000_000..<50_000_000: // 10MB to < 50MB + return "\(formattedFileSize(sizeInBytes: 10_000_000))-\(formattedFileSize(sizeInBytes: 50_000_000))" + case 50_000_000...100_000_000: // 50MB-100MB + return "\(formattedFileSize(sizeInBytes: 50_000_000))-\(formattedFileSize(sizeInBytes: 100_000_000))" + case 100_000_000...: // Greater than or equal to 100MB + return "> \(formattedFileSize(sizeInBytes: 100_000_000))" + default: + return "" + } + } + + func formattedFileSize(sizeInBytes: Int) -> String { + let sizeInMB = Measurement(value: Double(sizeInBytes), unit: UnitInformationStorage.bytes).converted(to: .megabytes) + + let formatter = MeasurementFormatter() + formatter.unitOptions = .providedUnit + formatter.numberFormatter.maximumFractionDigits = 2 + + return formatter.string(from: sizeInMB) + } + // MARK: - Listening History func listeningHistoryEpisodes() -> [ArraySection] { From 94f62e50bd36313734f08fb0bb53a15415adbb1c Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Tue, 3 Dec 2024 16:36:57 -0700 Subject: [PATCH 4/4] Add AnalyticsDescribable --- .../Analytics/AnalyticsDescribable+Modules.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/podcasts/Analytics/AnalyticsDescribable+Modules.swift b/podcasts/Analytics/AnalyticsDescribable+Modules.swift index 07d537844b..276fb7ac10 100644 --- a/podcasts/Analytics/AnalyticsDescribable+Modules.swift +++ b/podcasts/Analytics/AnalyticsDescribable+Modules.swift @@ -174,3 +174,18 @@ extension SocialAuthProvider: AnalyticsDescribable { } } } + +extension DownloadsSort: AnalyticsDescribable { + var analyticsDescription: String { + switch self { + case .newestToOldest: + return "newest_to_oldest" + case .oldestToNewest: + return "oldest_to_newest" + case .largestToSmallest: + return "largest_to_smallest" + case .smallestToLargest: + return "smallest_to_largest" + } + } +}