diff --git a/JellyfinPlayer/ItemView/ItemView.swift b/JellyfinPlayer/ItemView/ItemView.swift index 1fb4af4a..445d4098 100644 --- a/JellyfinPlayer/ItemView/ItemView.swift +++ b/JellyfinPlayer/ItemView/ItemView.swift @@ -55,6 +55,39 @@ private struct ItemView: View { } } + @ViewBuilder + var toolbarItemContent: some View { + switch viewModel.item.itemType { + case .season: + Menu { + Button { + (viewModel as? SeasonItemViewModel)?.routeToSeriesItem() + } label: { + Label("Show Series", systemImage: "text.below.photo") + } + } label: { + Image(systemName: "ellipsis.circle") + } + case .episode: + Menu { + Button { + (viewModel as? EpisodeItemViewModel)?.routeToSeriesItem() + } label: { + Label("Show Series", systemImage: "text.below.photo") + } + Button { + (viewModel as? EpisodeItemViewModel)?.routeToSeasonItem() + } label: { + Label("Show Season", systemImage: "square.fill.text.grid.1x2") + } + } label: { + Image(systemName: "ellipsis.circle") + } + default: + EmptyView() + } + } + var body: some View { Group { if hSizeClass == .compact && vSizeClass == .regular { @@ -71,5 +104,10 @@ private struct ItemView: View { guard flag else { return } self.itemRouter.route(to: \.videoPlayer, viewModel.item) } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + toolbarItemContent + } + } } } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index dc27b5da..a9c63a97 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -110,18 +110,3 @@ struct LibrarySearchView: View { } } } - -private extension ItemType { - var localized: String { - switch self { - case .episode: - return "Episodes" - case .movie: - return "Movies" - case .series: - return "Shows" - default: - return "" - } - } -} diff --git a/Shared/ViewModels/EpisodeItemViewModel.swift b/Shared/ViewModels/EpisodeItemViewModel.swift index 86e3d8f8..eaed1330 100644 --- a/Shared/ViewModels/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/EpisodeItemViewModel.swift @@ -10,15 +10,41 @@ import Combine import Foundation import JellyfinAPI +import Stinsen final class EpisodeItemViewModel: ItemViewModel { - + @RouterObject var itemRouter: ItemCoordinator.Router? + override func getItemDisplayName() -> String { guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" } return "\(episodeLocator)\n\(item.name ?? "")" } - + override func shouldDisplayRuntime() -> Bool { return false } + + func routeToSeasonItem() { + guard let id = item.seasonId else { return } + UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] item in + self?.itemRouter?.route(to: \.item, item) + }) + .store(in: &cancellables) + } + + func routeToSeriesItem() { + guard let id = item.seriesId else { return } + UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] item in + self?.itemRouter?.route(to: \.item, item) + }) + .store(in: &cancellables) + } } diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index e5e73ec8..e2919306 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -10,9 +10,10 @@ import Combine import Foundation import JellyfinAPI +import Stinsen final class SeasonItemViewModel: ItemViewModel { - + @RouterObject var itemRouter: ItemCoordinator.Router? @Published private(set) var episodes: [BaseItemDto] = [] override init(item: BaseItemDto) { @@ -21,7 +22,7 @@ final class SeasonItemViewModel: ItemViewModel { requestEpisodes() } - + override func playButtonText() -> String { guard let playButtonItem = playButtonItem else { return "Play" } guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return "Play" } @@ -29,7 +30,8 @@ final class SeasonItemViewModel: ItemViewModel { } private func requestEpisodes() { - LogManager.shared.log.debug("Getting episodes in season \(self.item.id!) (\(self.item.name!)) of show \(self.item.seriesId!) (\(self.item.seriesName!))") + LogManager.shared.log + .debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))") TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "") @@ -39,20 +41,20 @@ final class SeasonItemViewModel: ItemViewModel { }, receiveValue: { [weak self] response in self?.episodes = response.items ?? [] LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes") - + self?.setNextUpInSeason() }) .store(in: &cancellables) } - + // Sets the play button item to the "Next up" in the season based upon // the watched status of episodes in the season. // Default to the first episode of the season if all have been watched. private func setNextUpInSeason() { - guard episodes.count > 0 else { return } - + guard !episodes.isEmpty else { return } + var firstUnwatchedSearch: BaseItemDto? - + for episode in episodes { guard let played = episode.userData?.played else { continue } if !played { @@ -60,7 +62,7 @@ final class SeasonItemViewModel: ItemViewModel { break } } - + if let firstUnwatched = firstUnwatchedSearch { playButtonItem = firstUnwatched } else { @@ -68,4 +70,16 @@ final class SeasonItemViewModel: ItemViewModel { playButtonItem = firstEpisode } } + + func routeToSeriesItem() { + guard let id = item.seriesId else { return } + UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] item in + self?.itemRouter?.route(to: \.item, item) + }) + .store(in: &cancellables) + } }