diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 7011a1ec..77a27e13 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -13,6 +13,9 @@ import UIKit extension BaseItemDto { func createVideoPlayerViewModel() -> AnyPublisher { + + LogManager.shared.log.debug("Creating video player view model for item: \(id ?? "")") + let builder = DeviceProfileBuilder() // TODO: fix bitrate settings builder.setMaxBitrate(bitrate: 60_000_000) diff --git a/Shared/Singleton/SwiftfinNotificationCenter.swift b/Shared/Singleton/SwiftfinNotificationCenter.swift index ae01e73a..cbf99bcd 100644 --- a/Shared/Singleton/SwiftfinNotificationCenter.swift +++ b/Shared/Singleton/SwiftfinNotificationCenter.swift @@ -20,5 +20,8 @@ enum SwiftfinNotificationCenter { static let processDeepLink = Notification.Name("processDeepLink") static let didPurge = Notification.Name("didPurge") static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI") + + // Send with an item id to check if current item for item views + static let didSendStopReport = Notification.Name("didSendStopReport") } } diff --git a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift index bf3cd6a1..fdd6f902 100644 --- a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift @@ -29,10 +29,6 @@ final class EpisodeItemViewModel: ItemViewModel { return "\(episodeLocator)\n\(item.name ?? "")" } - override func shouldDisplayRuntime() -> Bool { - false - } - func getEpisodeSeries() { guard let id = item.seriesId else { return } UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) @@ -44,4 +40,29 @@ final class EpisodeItemViewModel: ItemViewModel { }) .store(in: &cancellables) } + + override func updateItem() { + ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id, + limit: 1, + fields: [ + .primaryImageAspectRatio, + .seriesPrimaryImage, + .seasonUserData, + .overview, + .genres, + .people, + .chapters, + ], + enableUserData: true, + ids: [item.id ?? ""]) + .sink { completion in + self.handleAPIRequestError(completion: completion) + } receiveValue: { response in + if let item = response.items?.first { + self.item = item + self.playButtonItem = item + } + } + .store(in: &cancellables) + } } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 0d820b17..86fa3764 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -54,9 +54,27 @@ class ItemViewModel: ViewModel { getSimilarItems() + SwiftfinNotificationCenter.main.addObserver(self, + selector: #selector(receivedStopReport(_:)), + name: SwiftfinNotificationCenter.Keys.didSendStopReport, + object: nil) + refreshItemVideoPlayerViewModel(for: item) } + @objc + private func receivedStopReport(_ notification: NSNotification) { + guard let itemID = notification.object as? String else { return } + + if itemID == item.id { + updateItem() + } else { + // Remove if necessary. Note that this cannot be in deinit as + // holding as an observer won't allow the object to be deinit-ed + SwiftfinNotificationCenter.main.removeObserver(self) + } + } + func refreshItemVideoPlayerViewModel(for item: BaseItemDto) { item.createVideoPlayerViewModel() .sink { completion in @@ -139,4 +157,7 @@ class ItemViewModel: ViewModel { .store(in: &cancellables) } } + + // Overridden by subclasses + func updateItem() {} } diff --git a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift index e8c9e1b8..08755376 100644 --- a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift @@ -10,4 +10,30 @@ import Combine import Foundation import JellyfinAPI -final class MovieItemViewModel: ItemViewModel {} +final class MovieItemViewModel: ItemViewModel { + + override func updateItem() { + ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id, + limit: 1, + fields: [ + .primaryImageAspectRatio, + .seriesPrimaryImage, + .seasonUserData, + .overview, + .genres, + .people, + .chapters, + ], + enableUserData: true, + ids: [item.id ?? ""]) + .sink { completion in + self.handleAPIRequestError(completion: completion) + } receiveValue: { response in + if let item = response.items?.first { + self.item = item + self.playButtonItem = item + } + } + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 0bba10b5..8b3562b4 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -247,7 +247,7 @@ extension VideoPlayerViewModel { adjacentTo: item.id, limit: 3) .sink(receiveCompletion: { completion in - print(completion) + self.handleAPIRequestError(completion: completion) }, receiveValue: { response in // 4 possible states: @@ -510,6 +510,8 @@ extension VideoPlayerViewModel { self.handleAPIRequestError(completion: completion) } receiveValue: { _ in LogManager.shared.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")") + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSendStopReport, + object: self.item.id) } .store(in: &cancellables) } @@ -536,3 +538,13 @@ extension VideoPlayerViewModel { return newURL.url! } } + +// MARK: Equatable + +extension VideoPlayerViewModel: Equatable { + + static func == (lhs: VideoPlayerViewModel, rhs: VideoPlayerViewModel) -> Bool { + lhs.item.id == rhs.item.id && + lhs.item.userData?.playbackPositionTicks == rhs.item.userData?.playbackPositionTicks + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index afd265f7..6d365268 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2773,7 +2773,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2810,7 +2810,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2841,7 +2841,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2868,7 +2868,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Swiftfin/Views/ItemView/ItemViewBody.swift b/Swiftfin/Views/ItemView/ItemViewBody.swift index 84ce8756..efcc03f9 100644 --- a/Swiftfin/Views/ItemView/ItemViewBody.swift +++ b/Swiftfin/Views/ItemView/ItemViewBody.swift @@ -54,8 +54,7 @@ struct ItemViewBody: View { topBarView: { L10n.seasons.text .fontWeight(.semibold) - .padding(.bottom) - .padding(.horizontal) + .padding() }, selectedAction: { season in itemRouter.route(to: \.item, season) }) @@ -63,14 +62,14 @@ struct ItemViewBody: View { // MARK: Genres - if let genres = viewModel.item.genreItems { - PillHStackView(title: L10n.genres, - items: genres, - selectedAction: { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - }) - .padding(.bottom) - } + if let genres = viewModel.item.genreItems { + PillHStackView(title: L10n.genres, + items: genres, + selectedAction: { genre in + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + }) + .padding(.bottom) + } // MARK: Studios @@ -120,7 +119,7 @@ struct ItemViewBody: View { // MARK: Cast & Crew if showCastAndCrew { - if let castAndCrew = viewModel.item.people, castAndCrew.count > 0 { + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }, topBarView: { L10n.castAndCrew.text diff --git a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift index a1fa20e1..f3a7b73c 100644 --- a/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift +++ b/Swiftfin/Views/ItemView/Landscape/ItemLandscapeMainView.swift @@ -14,6 +14,8 @@ struct ItemLandscapeMainView: View { var itemRouter: ItemCoordinator.Router @EnvironmentObject private var viewModel: ItemViewModel + @State + private var playButtonText: String = "" // MARK: innerBody @@ -72,6 +74,9 @@ struct ItemLandscapeMainView: View { } } } + .onAppear { + playButtonText = viewModel.playButtonText() + } } // MARK: body diff --git a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift index f3bb1663..6d610b25 100644 --- a/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift +++ b/Swiftfin/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift @@ -15,6 +15,8 @@ struct PortraitHeaderOverlayView: View { var itemRouter: ItemCoordinator.Router @EnvironmentObject private var viewModel: ItemViewModel + @State + private var playButtonText: String = "" var body: some View { VStack(alignment: .leading) { @@ -37,9 +39,9 @@ struct PortraitHeaderOverlayView: View { .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 10) - if viewModel.item.itemType.showDetails { - // MARK: Runtime + // MARK: Details + HStack { if viewModel.shouldDisplayRuntime() { if let runtime = viewModel.item.getItemRuntime() { Text(runtime) @@ -49,11 +51,7 @@ struct PortraitHeaderOverlayView: View { .lineLimit(1) } } - } - // MARK: Details - - HStack { if let productionYear = viewModel.item.productionYear { Text(String(productionYear)) .font(.subheadline) @@ -88,7 +86,7 @@ struct PortraitHeaderOverlayView: View { Image(systemName: "play.fill") .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) .font(.system(size: 20)) - Text(viewModel.playButtonText()) + Text(playButtonText) .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) .font(.callout) .fontWeight(.semibold) @@ -137,7 +135,10 @@ struct PortraitHeaderOverlayView: View { } }.padding(.top, 8) } - .padding(.horizontal, 16) + .onAppear { + playButtonText = viewModel.playButtonText() + } + .padding(.horizontal) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) } }