From 24a0cc15a2dc98a6898ce106379842eb27a980f6 Mon Sep 17 00:00:00 2001 From: Maximilian Bauer Date: Thu, 2 Jan 2025 22:16:21 +0100 Subject: [PATCH] RadiosVC: show play/random button and support context --- Amperfy/CarPlaySceneDelegate.swift | 25 ++++++++++--- .../ViewController/AlbumDetailVC.swift | 5 +-- Amperfy/Screens/ViewController/RadiosVC.swift | 37 +++++++++++++++---- AmperfyKit/Player/PlayerFacade.swift | 2 +- AmperfyKit/Storage/LibraryStorage.swift | 9 +++++ .../BasicFetchedResultsController.swift | 9 +++++ .../FetchedResultsControllers.swift | 3 +- 7 files changed, 70 insertions(+), 20 deletions(-) diff --git a/Amperfy/CarPlaySceneDelegate.swift b/Amperfy/CarPlaySceneDelegate.swift index 70dda195..a9ffbcda 100644 --- a/Amperfy/CarPlaySceneDelegate.swift +++ b/Amperfy/CarPlaySceneDelegate.swift @@ -542,18 +542,31 @@ class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { private func createRadioItems(from fetchedController: BasicFetchedResultsController?) -> [CPListTemplateItem] { var items = [CPListTemplateItem]() guard let fetchedController = fetchedController else { return items } - guard let fetchedRadios = fetchedController.fetchedObjects else { return items } - let itemCount = min(fetchedRadios.count, CPListTemplate.maximumSectionCount) + let itemCount = min(fetchedRadios.count, CPListTemplate.maximumSectionCount-2) guard itemCount > 0 else { return items } - for radioIndex in 0...(itemCount-1) { - let radioMO = fetchedRadios[radioIndex] - let radio = Radio(managedObject: radioMO) - let listItem = createDetailTemplate(for: radio, playContext: PlayContext(containable: radio), isTrackDisplayed: false) + let radios = fetchedRadios.prefix(itemCount).compactMap{ Radio(managedObject: $0) } + + items.append(self.createPlayRandomListItem(playContext: PlayContext(name: "Radios", playables: radios))) + for (index, radio) in radios.enumerated() { + let listItem = createDetailTemplate(for: radio, playContext: PlayContext(name: "Radios", index: index, playables: Array(radios)), isTrackDisplayed: false) items.append(listItem) } return items } + + private func createPlayRandomListItem(playContext: PlayContext, text: String = "Random") -> CPListItem { + let img = UIImage.createArtwork(with: UIImage.shuffle, iconSizeType: .small, theme: appDelegate.storage.settings.themePreference, switchColors: true).carPlayImage(carTraitCollection: traits) + let listItem = CPListItem(text: text, detailText: nil, image: img) + listItem.handler = { [weak self] item, completion in + guard let `self` = self else { completion(); return } + self.appDelegate.player.play(context: playContext.getWithShuffledIndex()) + self.displayNowPlaying() { + completion() + } + } + return listItem + } private func createPlayShuffledListItem(playContext: PlayContext, text: String = "Shuffle") -> CPListItem { let img = UIImage.createArtwork(with: UIImage.shuffle, iconSizeType: .small, theme: appDelegate.storage.settings.themePreference, switchColors: true).carPlayImage(carTraitCollection: traits) diff --git a/Amperfy/Screens/ViewController/AlbumDetailVC.swift b/Amperfy/Screens/ViewController/AlbumDetailVC.swift index 6c436365..2cc5f408 100644 --- a/Amperfy/Screens/ViewController/AlbumDetailVC.swift +++ b/Amperfy/Screens/ViewController/AlbumDetailVC.swift @@ -89,10 +89,7 @@ class AlbumDetailVC: SingleSnapshotFetchedResultsTableViewController { containableAtIndexPathCallback = { (indexPath) in return self.fetchedResultsController.getWrappedEntity(at: indexPath) } - playContextAtIndexPathCallback = { (indexPath) in - let entity = self.fetchedResultsController.getWrappedEntity(at: indexPath) - return PlayContext(containable: entity) - } + playContextAtIndexPathCallback = self.convertIndexPathToPlayContext swipeCallback = { (indexPath, completionHandler) in let song = self.fetchedResultsController.getWrappedEntity(at: indexPath) let playContext = self.convertIndexPathToPlayContext(songIndexPath: indexPath) diff --git a/Amperfy/Screens/ViewController/RadiosVC.swift b/Amperfy/Screens/ViewController/RadiosVC.swift index 285ef7f3..6bb89d1a 100644 --- a/Amperfy/Screens/ViewController/RadiosVC.swift +++ b/Amperfy/Screens/ViewController/RadiosVC.swift @@ -31,7 +31,8 @@ class RadiosVC: SingleFetchedResultsTableViewController { } private var fetchedResultsController: RadiosFetchedResultsController! - + private var detailHeaderView: LibraryElementDetailTableHeaderView? + override func viewDidLoad() { super.viewDidLoad() @@ -50,17 +51,20 @@ class RadiosVC: SingleFetchedResultsTableViewController { tableView.rowHeight = PlayableTableCell.rowHeight tableView.estimatedRowHeight = PlayableTableCell.rowHeight -#if !targetEnvironment(macCatalyst) + let playShuffleConfig = PlayShuffleInfoConfiguration( + infoCB: { "\(self.fetchedResultsController.fetchedObjects?.count ?? 0) Radio\((self.fetchedResultsController.fetchedObjects?.count ?? 0) == 1 ? "" : "s")" }, + playContextCb: self.handleHeaderPlay, + player: appDelegate.player, + isInfoAlwaysHidden: false, + isShuffleOnContextNeccessary: false, + shuffleContextCb: self.handleHeaderShuffle) + detailHeaderView = LibraryElementDetailTableHeaderView.createTableHeader(rootView: self, configuration: playShuffleConfig) self.refreshControl?.addTarget(self, action: #selector(Self.handleRefresh), for: UIControl.Event.valueChanged) -#endif containableAtIndexPathCallback = { (indexPath) in return self.fetchedResultsController.getWrappedEntity(at: indexPath) } - playContextAtIndexPathCallback = { (indexPath) in - let entity = self.fetchedResultsController.getWrappedEntity(at: indexPath) - return PlayContext(containable: entity) - } + playContextAtIndexPathCallback = convertIndexPathToPlayContext swipeCallback = { (indexPath, completionHandler) in let radio = self.fetchedResultsController.getWrappedEntity(at: indexPath) let playContext = self.convertIndexPathToPlayContext(radioIndexPath: indexPath) @@ -81,10 +85,23 @@ class RadiosVC: SingleFetchedResultsTableViewController { }.catch { error in self.appDelegate.eventLogger.report(topic: "Radios Sync", error: error) }.finally { + self.detailHeaderView?.refresh() self.updateSearchResults(for: self.searchController) } } + public func handleHeaderPlay() -> PlayContext { + guard let displayedRadiosMO = self.fetchedResultsController.fetchedObjects else { return PlayContext(name: sceneTitle ?? "", playables: []) } + let radios = displayedRadiosMO.prefix(appDelegate.player.maxSongsToAddOnce).compactMap{ Radio(managedObject: $0) } + return PlayContext(name: sceneTitle ?? "", playables: radios) + } + + public func handleHeaderShuffle() -> PlayContext { + guard let displayedRadiosMO = self.fetchedResultsController.fetchedObjects else { return PlayContext(name: sceneTitle ?? "", playables: []) } + let radios = displayedRadiosMO.prefix(appDelegate.player.maxSongsToAddOnce).compactMap{ Radio(managedObject: $0) } + return PlayContext(name: sceneTitle ?? "", index: Int.random(in: 0.. UITableViewCell { let cell: PlayableTableCell = dequeueCell(for: tableView, at: indexPath) let radio = fetchedResultsController.getWrappedEntity(at: indexPath) @@ -101,8 +118,11 @@ class RadiosVC: SingleFetchedResultsTableViewController { } func convertIndexPathToPlayContext(radioIndexPath: IndexPath) -> PlayContext? { + guard let radios = self.fetchedResultsController.getContextRadios() + else { return nil } let selectedRadio = self.fetchedResultsController.getWrappedEntity(at: radioIndexPath) - return PlayContext(containable: selectedRadio) + guard let playContextIndex = radios.firstIndex(of: selectedRadio) else { return nil } + return PlayContext(name: sceneTitle ?? "", index: playContextIndex, playables: radios) } func convertCellViewToPlayContext(cell: UITableViewCell) -> PlayContext? { @@ -132,6 +152,7 @@ class RadiosVC: SingleFetchedResultsTableViewController { }.catch { error in self.appDelegate.eventLogger.report(topic: "Radios Sync", error: error) }.finally { + self.detailHeaderView?.refresh() self.updateSearchResults(for: self.searchController) #if !targetEnvironment(macCatalyst) self.refreshControl?.endRefreshing() diff --git a/AmperfyKit/Player/PlayerFacade.swift b/AmperfyKit/Player/PlayerFacade.swift index 41b66684..62214ced 100644 --- a/AmperfyKit/Player/PlayerFacade.swift +++ b/AmperfyKit/Player/PlayerFacade.swift @@ -94,7 +94,7 @@ public struct PlayContext { return playables[index] } - func getWithShuffledIndex() -> PlayContext { + public func getWithShuffledIndex() -> PlayContext { guard !isKeepIndexDuringShuffle else { return self } return PlayContext(name: name, index: Int.random(in: 0...playables.count-1), playables: playables) } diff --git a/AmperfyKit/Storage/LibraryStorage.swift b/AmperfyKit/Storage/LibraryStorage.swift index 50dadf62..8dd165e8 100644 --- a/AmperfyKit/Storage/LibraryStorage.swift +++ b/AmperfyKit/Storage/LibraryStorage.swift @@ -920,6 +920,7 @@ public class LibraryStorage: PlayableFileCachable { public func getRadios() -> [Radio] { let fetchRequest = RadioMO.identifierSortedFetchRequest + fetchRequest.predicate = RadioMO.excludeServerDeleteRadiosFetchPredicate let foundRadios = try? context.fetch(fetchRequest) let radios = foundRadios?.compactMap{ Radio(managedObject: $0) } return radios ?? [Radio]() @@ -992,6 +993,14 @@ public class LibraryStorage: PlayableFileCachable { return wrapped ?? [Playlist]() } + public func getSearchRadiosPredicate(searchText: String) -> NSPredicate { + let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + RadioMO.excludeServerDeleteRadiosFetchPredicate, + RadioMO.getIdentifierBasedSearchPredicate(searchText: searchText) + ]) + return predicate + } + public func getSearchSongsPredicate(searchText: String, onlyCached: Bool, displayFilter: DisplayCategoryFilter) -> NSPredicate { let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ SongMO.excludeServerDeleteUncachedSongsFetchPredicate, diff --git a/AmperfyKit/Storage/ResultController/BasicFetchedResultsController.swift b/AmperfyKit/Storage/ResultController/BasicFetchedResultsController.swift index f1b951f6..54b566db 100644 --- a/AmperfyKit/Storage/ResultController/BasicFetchedResultsController.swift +++ b/AmperfyKit/Storage/ResultController/BasicFetchedResultsController.swift @@ -323,6 +323,15 @@ extension BasicFetchedResultsController where ResultType == RadioMO { let radioMO = fetchResultsController.object(at: indexPath) as! ResultType return Radio(managedObject: radioMO) } + + public func getContextRadios() -> [AbstractPlayable]? { + guard let basicPredicate = defaultPredicate else { return nil } + let cachedFetchRequest = fetchResultsController.fetchRequest.copy() as! NSFetchRequest + cachedFetchRequest.predicate = basicPredicate + let radiosMO = try? coreDataCompanion.context.fetch(cachedFetchRequest) + let radios = radiosMO?.compactMap{ Radio(managedObject: $0) } + return radios + } } extension BasicFetchedResultsController where ResultType == PlaylistMO { diff --git a/AmperfyKit/Storage/ResultController/FetchedResultsControllers.swift b/AmperfyKit/Storage/ResultController/FetchedResultsControllers.swift index d8ee5130..53955eb3 100644 --- a/AmperfyKit/Storage/ResultController/FetchedResultsControllers.swift +++ b/AmperfyKit/Storage/ResultController/FetchedResultsControllers.swift @@ -604,7 +604,8 @@ public class RadiosFetchedResultsController: CachedFetchedResultsController 0 { - search(predicate: RadioMO.excludeServerDeleteRadiosFetchPredicate) + let predicate = coreDataCompanion.library.getSearchRadiosPredicate(searchText: searchText) + search(predicate: predicate) } else { showAllResults() }