initial iOS missing items
This commit is contained in:
parent
0297367497
commit
6771d3d2c9
|
@ -22,6 +22,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
var overlaySettings = makeOverlaySettings
|
var overlaySettings = makeOverlaySettings
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var experimentalSettings = makeExperimentalSettings
|
var experimentalSettings = makeExperimentalSettings
|
||||||
|
@Route(.push)
|
||||||
|
var missingSettings = makeMissingSettings
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeServerDetail() -> some View {
|
func makeServerDetail() -> some View {
|
||||||
|
@ -39,6 +41,11 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
ExperimentalSettingsView()
|
ExperimentalSettingsView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeMissingSettings() -> some View {
|
||||||
|
MissingItemsSettingsView()
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
|
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
|
||||||
|
|
|
@ -289,4 +289,31 @@ public extension BaseItemDto {
|
||||||
|
|
||||||
return mediaItems
|
return mediaItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Missing and Unaired
|
||||||
|
|
||||||
|
var missing: Bool {
|
||||||
|
locationType == .virtual
|
||||||
|
}
|
||||||
|
|
||||||
|
var unaired: Bool {
|
||||||
|
if let premierDate = premiereDate {
|
||||||
|
return premierDate > Date()
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var airDateLabel: String? {
|
||||||
|
guard let premiereDateFormatted = premiereDateFormatted else { return nil }
|
||||||
|
return L10n.airWithDate(premiereDateFormatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
var premiereDateFormatted: String? {
|
||||||
|
guard let premiereDate = premiereDate else { return nil }
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .medium
|
||||||
|
return dateFormatter.string(from: premiereDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@ internal enum L10n {
|
||||||
internal static let accessibility = L10n.tr("Localizable", "accessibility")
|
internal static let accessibility = L10n.tr("Localizable", "accessibility")
|
||||||
/// Add URL
|
/// Add URL
|
||||||
internal static let addURL = L10n.tr("Localizable", "addURL")
|
internal static let addURL = L10n.tr("Localizable", "addURL")
|
||||||
|
/// Airs %s
|
||||||
|
internal static func airWithDate(_ p1: UnsafePointer<CChar>) -> String {
|
||||||
|
return L10n.tr("Localizable", "airWithDate", p1)
|
||||||
|
}
|
||||||
/// All Genres
|
/// All Genres
|
||||||
internal static let allGenres = L10n.tr("Localizable", "allGenres")
|
internal static let allGenres = L10n.tr("Localizable", "allGenres")
|
||||||
/// All Media
|
/// All Media
|
||||||
|
@ -144,6 +148,8 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
/// Media
|
/// Media
|
||||||
internal static let media = L10n.tr("Localizable", "media")
|
internal static let media = L10n.tr("Localizable", "media")
|
||||||
|
/// Missing
|
||||||
|
internal static let missing = L10n.tr("Localizable", "missing")
|
||||||
/// More Like This
|
/// More Like This
|
||||||
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
|
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
|
||||||
/// Movies
|
/// Movies
|
||||||
|
@ -340,6 +346,8 @@ internal enum L10n {
|
||||||
internal static let tvShows = L10n.tr("Localizable", "tvShows")
|
internal static let tvShows = L10n.tr("Localizable", "tvShows")
|
||||||
/// Unable to connect to server
|
/// Unable to connect to server
|
||||||
internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer")
|
internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer")
|
||||||
|
/// Unaired
|
||||||
|
internal static let unaired = L10n.tr("Localizable", "unaired")
|
||||||
/// Unauthorized
|
/// Unauthorized
|
||||||
internal static let unauthorized = L10n.tr("Localizable", "unauthorized")
|
internal static let unauthorized = L10n.tr("Localizable", "unauthorized")
|
||||||
/// Unauthorized user
|
/// Unauthorized user
|
||||||
|
|
|
@ -58,6 +58,10 @@ extension Defaults.Keys {
|
||||||
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
||||||
|
// Should show missing seasons and episodes
|
||||||
|
static let shouldShowMissingSeasons = Key<Bool>("shouldShowMissingSeasons", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
||||||
// Should show video player items in overlay menu
|
// Should show video player items in overlay menu
|
||||||
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
|
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
|
@ -6,36 +6,26 @@
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class EpisodesRowViewModel: ViewModel {
|
protocol EpisodesRowManager: ViewModel {
|
||||||
|
var item: BaseItemDto { get }
|
||||||
|
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] { get set }
|
||||||
|
var selectedSeason: BaseItemDto? { get set }
|
||||||
|
func retrieveSeasons()
|
||||||
|
func retrieveEpisodesForSeason(_ season: BaseItemDto)
|
||||||
|
func select(season: BaseItemDto)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Protocol these viewmodels for generalization instead of Episode
|
extension EpisodesRowManager {
|
||||||
|
|
||||||
@ObservedObject
|
// Also retrieves the current season episodes if available
|
||||||
var episodeItemViewModel: EpisodeItemViewModel
|
func retrieveSeasons() {
|
||||||
@Published
|
TvShowsAPI.getSeasons(seriesId: item.seriesId ?? "",
|
||||||
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
@Published
|
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false)
|
||||||
var selectedSeason: BaseItemDto? {
|
|
||||||
willSet {
|
|
||||||
if seasonsEpisodes[newValue!]!.isEmpty {
|
|
||||||
retrieveEpisodesForSeason(newValue!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(episodeItemViewModel: EpisodeItemViewModel) {
|
|
||||||
self.episodeItemViewModel = episodeItemViewModel
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
retrieveSeasons()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func retrieveSeasons() {
|
|
||||||
TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "",
|
|
||||||
userId: SessionManager.main.currentLogin.user.id)
|
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
|
@ -43,21 +33,23 @@ final class EpisodesRowViewModel: ViewModel {
|
||||||
seasons.forEach { season in
|
seasons.forEach { season in
|
||||||
self.seasonsEpisodes[season] = []
|
self.seasonsEpisodes[season] = []
|
||||||
|
|
||||||
if season.id == self.episodeItemViewModel.item.seasonId ?? "" {
|
if season.id == self.item.seasonId ?? "" {
|
||||||
self.selectedSeason = season
|
self.selectedSeason = season
|
||||||
|
self.retrieveEpisodesForSeason(season)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func retrieveEpisodesForSeason(_ season: BaseItemDto) {
|
func retrieveEpisodesForSeason(_ season: BaseItemDto) {
|
||||||
guard let seasonID = season.id else { return }
|
guard let seasonID = season.id else { return }
|
||||||
|
|
||||||
TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "",
|
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "",
|
||||||
userId: SessionManager.main.currentLogin.user.id,
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seasonId: seasonID)
|
seasonId: seasonID,
|
||||||
|
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
@ -66,6 +58,14 @@ final class EpisodesRowViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func select(season: BaseItemDto) {
|
||||||
|
self.selectedSeason = season
|
||||||
|
|
||||||
|
if seasonsEpisodes[season]!.isEmpty {
|
||||||
|
retrieveEpisodesForSeason(season)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SingleSeasonEpisodesRowViewModel: ViewModel {
|
final class SingleSeasonEpisodesRowViewModel: ViewModel {
|
|
@ -11,17 +11,22 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
final class EpisodeItemViewModel: ItemViewModel {
|
final class EpisodeItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var itemRouter: ItemCoordinator.Router?
|
var itemRouter: ItemCoordinator.Router?
|
||||||
@Published
|
@Published
|
||||||
var series: BaseItemDto?
|
var series: BaseItemDto?
|
||||||
|
@Published
|
||||||
|
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
|
||||||
|
@Published
|
||||||
|
var selectedSeason: BaseItemDto?
|
||||||
|
|
||||||
override init(item: BaseItemDto) {
|
override init(item: BaseItemDto) {
|
||||||
super.init(item: item)
|
super.init(item: item)
|
||||||
|
|
||||||
getEpisodeSeries()
|
getEpisodeSeries()
|
||||||
|
retrieveSeasons()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func getItemDisplayName() -> String {
|
override func getItemDisplayName() -> String {
|
||||||
|
|
|
@ -41,7 +41,9 @@ class ItemViewModel: ViewModel {
|
||||||
|
|
||||||
switch item.itemType {
|
switch item.itemType {
|
||||||
case .episode, .movie:
|
case .episode, .movie:
|
||||||
self.playButtonItem = item
|
if !item.missing && !item.unaired {
|
||||||
|
self.playButtonItem = item
|
||||||
|
}
|
||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +78,9 @@ class ItemViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
|
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
|
||||||
|
guard item.itemType == .episode || item.itemType == .movie else { return }
|
||||||
|
guard !item.missing, !item.unaired else { return }
|
||||||
|
|
||||||
item.createVideoPlayerViewModel()
|
item.createVideoPlayerViewModel()
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
@ -87,6 +92,15 @@ class ItemViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func playButtonText() -> String {
|
func playButtonText() -> String {
|
||||||
|
|
||||||
|
if item.unaired {
|
||||||
|
return L10n.unaired
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.missing {
|
||||||
|
return L10n.missing
|
||||||
|
}
|
||||||
|
|
||||||
if let itemProgressString = item.getItemProgressString() {
|
if let itemProgressString = item.getItemProgressString() {
|
||||||
return itemProgressString
|
return itemProgressString
|
||||||
}
|
}
|
||||||
|
@ -103,7 +117,9 @@ class ItemViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSimilarItems() {
|
func getSimilarItems() {
|
||||||
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20,
|
LibraryAPI.getSimilarItems(itemId: item.id!,
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
limit: 10,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
@ -116,7 +132,8 @@ class ItemViewModel: ViewModel {
|
||||||
|
|
||||||
func updateWatchState() {
|
func updateWatchState() {
|
||||||
if isWatched {
|
if isWatched {
|
||||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
|
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: item.id!)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
@ -125,7 +142,8 @@ class ItemViewModel: ViewModel {
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
} else {
|
} else {
|
||||||
PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
|
PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
itemId: item.id!)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
final class SeasonItemViewModel: ItemViewModel {
|
final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var itemRouter: ItemCoordinator.Router?
|
var itemRouter: ItemCoordinator.Router?
|
||||||
|
@ -19,61 +19,75 @@ final class SeasonItemViewModel: ItemViewModel {
|
||||||
var episodes: [BaseItemDto] = []
|
var episodes: [BaseItemDto] = []
|
||||||
@Published
|
@Published
|
||||||
var seriesItem: BaseItemDto?
|
var seriesItem: BaseItemDto?
|
||||||
|
@Published
|
||||||
|
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
|
||||||
|
@Published
|
||||||
|
var selectedSeason: BaseItemDto?
|
||||||
|
|
||||||
override init(item: BaseItemDto) {
|
override init(item: BaseItemDto) {
|
||||||
super.init(item: item)
|
super.init(item: item)
|
||||||
|
|
||||||
getSeriesItem()
|
getSeriesItem()
|
||||||
requestEpisodes()
|
selectedSeason = item
|
||||||
|
retrieveSeasons()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func playButtonText() -> String {
|
override func playButtonText() -> String {
|
||||||
guard let playButtonItem = playButtonItem else { return L10n.play }
|
|
||||||
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
if item.unaired {
|
||||||
|
return L10n.unaired
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.missing {
|
||||||
|
return L10n.missing
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let playButtonItem = playButtonItem, let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
||||||
return episodeLocator
|
return episodeLocator
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestEpisodes() {
|
// private func requestEpisodes() {
|
||||||
LogManager.shared.log
|
// LogManager.shared.log
|
||||||
.debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
|
// .debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
|
||||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id,
|
// TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
// fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seasonId: item.id ?? "")
|
// seasonId: item.id ?? "")
|
||||||
.trackActivity(loading)
|
// .trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
// .sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
// self?.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { [weak self] response in
|
// }, receiveValue: { [weak self] response in
|
||||||
self?.episodes = response.items ?? []
|
// self?.episodes = response.items ?? []
|
||||||
LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes")
|
// LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes")
|
||||||
|
//
|
||||||
self?.setNextUpInSeason()
|
// self?.setNextUpInSeason()
|
||||||
})
|
// })
|
||||||
.store(in: &cancellables)
|
// .store(in: &cancellables)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Sets the play button item to the "Next up" in the season based upon
|
// Sets the play button item to the "Next up" in the season based upon
|
||||||
// the watched status of episodes in the season.
|
// the watched status of episodes in the season.
|
||||||
// Default to the first episode of the season if all have been watched.
|
// Default to the first episode of the season if all have been watched.
|
||||||
private func setNextUpInSeason() {
|
// private func setNextUpInSeason() {
|
||||||
guard !episodes.isEmpty else { return }
|
// guard !item.missing else { return }
|
||||||
|
// guard !episodes.isEmpty else { return }
|
||||||
var firstUnwatchedSearch: BaseItemDto?
|
//
|
||||||
|
// var firstUnwatchedSearch: BaseItemDto?
|
||||||
for episode in episodes {
|
//
|
||||||
guard let played = episode.userData?.played else { continue }
|
// for episode in episodes {
|
||||||
if !played {
|
// guard let played = episode.userData?.played else { continue }
|
||||||
firstUnwatchedSearch = episode
|
// if !played {
|
||||||
break
|
// firstUnwatchedSearch = episode
|
||||||
}
|
// break
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
if let firstUnwatched = firstUnwatchedSearch {
|
//
|
||||||
playButtonItem = firstUnwatched
|
// if let firstUnwatched = firstUnwatchedSearch {
|
||||||
} else {
|
// playButtonItem = firstUnwatched
|
||||||
guard let firstEpisode = episodes.first else { return }
|
// } else {
|
||||||
playButtonItem = firstEpisode
|
// guard let firstEpisode = episodes.first else { return }
|
||||||
}
|
// playButtonItem = firstEpisode
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
private func getSeriesItem() {
|
private func getSeriesItem() {
|
||||||
guard let seriesID = item.seriesId else { return }
|
guard let seriesID = item.seriesId else { return }
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
|
@ -23,8 +24,16 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func playButtonText() -> String {
|
override func playButtonText() -> String {
|
||||||
guard let playButtonItem = playButtonItem else { return L10n.play }
|
|
||||||
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
if item.unaired {
|
||||||
|
return L10n.unaired
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.missing {
|
||||||
|
return L10n.missing
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let playButtonItem = playButtonItem, let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
||||||
return episodeLocator
|
return episodeLocator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,12 +46,13 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
seriesId: self.item.id!, enableUserData: true)
|
seriesId: self.item.id!,
|
||||||
|
enableUserData: true)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { [weak self] response in
|
}, receiveValue: { [weak self] response in
|
||||||
if let nextUpItem = response.items?.first {
|
if let nextUpItem = response.items?.first, !nextUpItem.unaired, !nextUpItem.missing {
|
||||||
self?.playButtonItem = nextUpItem
|
self?.playButtonItem = nextUpItem
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -71,6 +81,7 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
|
LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
|
||||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id,
|
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
|
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false,
|
||||||
enableUserData: true)
|
enableUserData: true)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
|
|
@ -266,8 +266,8 @@
|
||||||
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||||
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||||
E10D87E0278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
E10D87E0278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||||
E10D87E227852FD000BD264C /* EpisodesRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */; };
|
E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowManager.swift */; };
|
||||||
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */; };
|
E10D87E327852FD000BD264C /* EpisodesRowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowManager.swift */; };
|
||||||
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
||||||
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
||||||
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
||||||
|
@ -331,6 +331,8 @@
|
||||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||||
|
E176DE6D278E30D2001EFD8D /* EpisodeRowCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */; };
|
||||||
|
E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */; };
|
||||||
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; };
|
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; };
|
||||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; };
|
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; };
|
||||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
||||||
|
@ -340,7 +342,6 @@
|
||||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
||||||
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; };
|
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; };
|
||||||
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; };
|
|
||||||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
E19169CE272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
||||||
E19169CF272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
E19169CF272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
||||||
E19169D0272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
E19169D0272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
|
||||||
|
@ -665,7 +666,7 @@
|
||||||
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
|
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
|
||||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
|
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
|
||||||
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
|
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
|
||||||
E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowViewModel.swift; sourceTree = "<group>"; };
|
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = "<group>"; };
|
||||||
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||||
|
@ -698,6 +699,8 @@
|
||||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
|
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
|
||||||
E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
|
E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
|
||||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeRowCard.swift; sourceTree = "<group>"; };
|
||||||
|
E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingItemsSettingsView.swift; sourceTree = "<group>"; };
|
||||||
E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = "<group>"; };
|
E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = "<group>"; };
|
||||||
E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = "<group>"; };
|
E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = "<group>"; };
|
||||||
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVLCOverlay.swift; sourceTree = "<group>"; };
|
E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVLCOverlay.swift; sourceTree = "<group>"; };
|
||||||
|
@ -705,7 +708,6 @@
|
||||||
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = "<group>"; };
|
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = "<group>"; };
|
||||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
|
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
|
||||||
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
|
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
|
||||||
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCardVStackView.swift; sourceTree = "<group>"; };
|
|
||||||
E19169CD272514760085832A /* HTTPScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPScheme.swift; sourceTree = "<group>"; };
|
E19169CD272514760085832A /* HTTPScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPScheme.swift; sourceTree = "<group>"; };
|
||||||
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitImageStackable.swift; sourceTree = "<group>"; };
|
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitImageStackable.swift; sourceTree = "<group>"; };
|
||||||
E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = "<group>"; };
|
E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = "<group>"; };
|
||||||
|
@ -855,7 +857,7 @@
|
||||||
children = (
|
children = (
|
||||||
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
|
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
|
||||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||||
E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */,
|
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */,
|
||||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||||
E107BB9127880A4000354E07 /* ItemViewModel */,
|
E107BB9127880A4000354E07 /* ItemViewModel */,
|
||||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
|
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
|
||||||
|
@ -1207,8 +1209,7 @@
|
||||||
53F866422687A45400DCD1D7 /* Components */ = {
|
53F866422687A45400DCD1D7 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
E176DE6E278E3522001EFD8D /* EpisodesRowView */,
|
||||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
|
||||||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
||||||
|
@ -1482,6 +1483,15 @@
|
||||||
path = ItemView;
|
path = ItemView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E176DE6E278E3522001EFD8D /* EpisodesRowView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
||||||
|
E176DE6C278E30D2001EFD8D /* EpisodeRowCard.swift */,
|
||||||
|
);
|
||||||
|
path = EpisodesRowView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E178859C2780F5300094FBCF /* tvOSSLider */ = {
|
E178859C2780F5300094FBCF /* tvOSSLider */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1637,6 +1647,7 @@
|
||||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
||||||
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */,
|
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */,
|
||||||
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
|
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
|
||||||
|
E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */,
|
||||||
);
|
);
|
||||||
path = SettingsView;
|
path = SettingsView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2223,7 +2234,7 @@
|
||||||
E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */,
|
E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */,
|
||||||
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
|
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
|
||||||
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
|
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
|
||||||
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
E10D87E327852FD000BD264C /* EpisodesRowManager.swift in Sources */,
|
||||||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||||
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
||||||
|
@ -2269,9 +2280,10 @@
|
||||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
||||||
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
||||||
|
E176DE6D278E30D2001EFD8D /* EpisodeRowCard.swift in Sources */,
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||||
E10D87E227852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */,
|
||||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
|
@ -2285,7 +2297,6 @@
|
||||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||||
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */,
|
|
||||||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
||||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
||||||
|
@ -2361,6 +2372,7 @@
|
||||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
||||||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
|
E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */,
|
||||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
|
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
|
||||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||||
|
@ -2773,7 +2785,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
EXCLUDED_ARCHS = "";
|
EXCLUDED_ARCHS = "";
|
||||||
|
@ -2810,7 +2822,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
EXCLUDED_ARCHS = "";
|
EXCLUDED_ARCHS = "";
|
||||||
|
@ -2841,7 +2853,7 @@
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -2868,7 +2880,7 @@
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 66;
|
CURRENT_PROJECT_VERSION = 66;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||||
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
//
|
|
||||||
// Swiftfin is subject to the terms of the Mozilla Public
|
|
||||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct EpisodeCardVStackView: View {
|
|
||||||
|
|
||||||
let items: [BaseItemDto]
|
|
||||||
let selectedAction: (BaseItemDto) -> Void
|
|
||||||
|
|
||||||
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
|
||||||
HStack {
|
|
||||||
ZStack {
|
|
||||||
if item.userData?.isFavorite ?? false {
|
|
||||||
Image(systemName: "circle.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.opacity(0.6)
|
|
||||||
Image(systemName: "heart.fill")
|
|
||||||
.foregroundColor(Color(.systemRed))
|
|
||||||
.font(.system(size: 10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.leading, 2)
|
|
||||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
|
||||||
.opacity(1)
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
if item.userData?.played ?? false {
|
|
||||||
Image(systemName: "circle.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.jellyfinPurple)
|
|
||||||
}
|
|
||||||
}.padding(2)
|
|
||||||
.opacity(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
selectedAction(item)
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
// MARK: Image
|
|
||||||
|
|
||||||
ImageView(src: item.getPrimaryImage(maxWidth: 150),
|
|
||||||
bh: item.getPrimaryImageBlurHash(),
|
|
||||||
failureInitials: item.failureInitials)
|
|
||||||
.frame(width: 150, height: 100)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.overlay(Rectangle()
|
|
||||||
.fill(Color.jellyfinPurple)
|
|
||||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
|
||||||
.padding(0), alignment: .bottomLeading)
|
|
||||||
.overlay(buildCardOverlayView(item: item), alignment: .topTrailing)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
|
|
||||||
// MARK: Title
|
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text(item.getEpisodeLocator() ?? "")
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if let runtime = item.getItemRuntime() {
|
|
||||||
Text(runtime)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Overview
|
|
||||||
|
|
||||||
Text(item.overview ?? "")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.lineLimit(4)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
//
|
|
||||||
// Swiftfin is subject to the terms of the Mozilla Public
|
|
||||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
//
|
|
||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct EpisodesRowView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
var itemRouter: ItemCoordinator.Router
|
|
||||||
@ObservedObject
|
|
||||||
var viewModel: EpisodesRowViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Menu {
|
|
||||||
ForEach(Array(viewModel.seasonsEpisodes.keys).sorted(by: { $0.name ?? "" < $1.name ?? "" }), id: \.self) { season in
|
|
||||||
Button {
|
|
||||||
viewModel.selectedSeason = season
|
|
||||||
} label: {
|
|
||||||
if season.id == viewModel.selectedSeason?.id {
|
|
||||||
Label(season.name ?? L10n.season, systemImage: "checkmark")
|
|
||||||
} else {
|
|
||||||
Text(season.name ?? L10n.season)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 5) {
|
|
||||||
Text(viewModel.selectedSeason?.name ?? L10n.unknown)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.fixedSize()
|
|
||||||
Image(systemName: "chevron.down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
ScrollViewReader { reader in
|
|
||||||
HStack(alignment: .top, spacing: 15) {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
Color.gray.ignoresSafeArea()
|
|
||||||
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
|
||||||
.frame(width: 200, height: 112)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("S-E-")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("--")
|
|
||||||
.font(.body)
|
|
||||||
.padding(.bottom, 1)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(width: 200)
|
|
||||||
.shadow(radius: 4, y: 2)
|
|
||||||
} else if let selectedSeason = viewModel.selectedSeason {
|
|
||||||
if viewModel.seasonsEpisodes[selectedSeason]!.isEmpty {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
|
|
||||||
Color.gray.ignoresSafeArea()
|
|
||||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
|
||||||
.frame(width: 200, height: 112)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("--")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
L10n.noEpisodesAvailable.text
|
|
||||||
.font(.body)
|
|
||||||
.padding(.bottom, 1)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(width: 200)
|
|
||||||
.shadow(radius: 4, y: 2)
|
|
||||||
} else {
|
|
||||||
ForEach(viewModel.seasonsEpisodes[selectedSeason]!, id: \.self) { episode in
|
|
||||||
Button {
|
|
||||||
itemRouter.route(to: \.item, episode)
|
|
||||||
} label: {
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
|
|
||||||
ImageView(src: episode.getBackdropImage(maxWidth: 200),
|
|
||||||
bh: episode.getBackdropImageBlurHash())
|
|
||||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
|
||||||
.frame(width: 200, height: 112)
|
|
||||||
.overlay {
|
|
||||||
if episode.id == viewModel.episodeItemViewModel.item.id {
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.stroke(Color.jellyfinPurple, lineWidth: 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(episode.getEpisodeLocator() ?? "")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(episode.name ?? "")
|
|
||||||
.font(.body)
|
|
||||||
.padding(.bottom, 1)
|
|
||||||
.lineLimit(2)
|
|
||||||
Text(episode.overview ?? "")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.fontWeight(.light)
|
|
||||||
.lineLimit(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(width: 200)
|
|
||||||
.shadow(radius: 4, y: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.id(episode.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.onChange(of: viewModel.selectedSeason) { _ in
|
|
||||||
if viewModel.selectedSeason?.id == viewModel.episodeItemViewModel.item.seasonId {
|
|
||||||
reader.scrollTo(viewModel.episodeItemViewModel.item.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: viewModel.seasonsEpisodes) { _ in
|
|
||||||
if viewModel.selectedSeason?.id == viewModel.episodeItemViewModel.item.seasonId {
|
|
||||||
reader.scrollTo(viewModel.episodeItemViewModel.item.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EpisodeRowCard: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var itemRouter: ItemCoordinator.Router
|
||||||
|
let viewModel: EpisodesRowManager
|
||||||
|
let episode: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
itemRouter.route(to: \.item, episode)
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
ImageView(src: episode.getBackdropImage(maxWidth: 200),
|
||||||
|
bh: episode.getBackdropImageBlurHash())
|
||||||
|
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||||
|
.frame(width: 200, height: 112)
|
||||||
|
.overlay {
|
||||||
|
if episode.id == viewModel.item.id {
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(Color.jellyfinPurple, lineWidth: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(episode.getEpisodeLocator() ?? "S-:E-")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(episode.name ?? L10n.noTitle)
|
||||||
|
.font(.body)
|
||||||
|
.padding(.bottom, 1)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
if episode.unaired {
|
||||||
|
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.lineLimit(3)
|
||||||
|
} else {
|
||||||
|
Text(episode.overview ?? "")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.lineLimit(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: 200)
|
||||||
|
.shadow(radius: 4, y: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EpisodesRowView<RowManager>: View where RowManager: EpisodesRowManager {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var itemRouter: ItemCoordinator.Router
|
||||||
|
@ObservedObject
|
||||||
|
var viewModel: RowManager
|
||||||
|
let onlyCurrentSeason: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Menu {
|
||||||
|
ForEach(Array(viewModel.seasonsEpisodes.keys).sorted(by: { $0.name ?? "" < $1.name ?? "" }), id: \.self) { season in
|
||||||
|
Button {
|
||||||
|
viewModel.selectedSeason = season
|
||||||
|
} label: {
|
||||||
|
if season.id == viewModel.selectedSeason?.id {
|
||||||
|
Label(season.name ?? L10n.season, systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text(season.name ?? L10n.season)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Text(viewModel.selectedSeason?.name ?? L10n.unknown)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.fixedSize()
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
ScrollViewReader { reader in
|
||||||
|
HStack(alignment: .top, spacing: 15) {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
Color.gray.ignoresSafeArea()
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||||
|
.frame(width: 200, height: 112)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("S-:E-")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("--")
|
||||||
|
.font(.body)
|
||||||
|
.padding(.bottom, 1)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: 200)
|
||||||
|
.shadow(radius: 4, y: 2)
|
||||||
|
} else if let selectedSeason = viewModel.selectedSeason {
|
||||||
|
if let seasonEpisodes = viewModel.seasonsEpisodes[selectedSeason] {
|
||||||
|
if seasonEpisodes.isEmpty {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
Color.gray.ignoresSafeArea()
|
||||||
|
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||||
|
.frame(width: 200, height: 112)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("--")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
L10n.noEpisodesAvailable.text
|
||||||
|
.font(.body)
|
||||||
|
.padding(.bottom, 1)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: 200)
|
||||||
|
.shadow(radius: 4, y: 2)
|
||||||
|
} else {
|
||||||
|
ForEach(seasonEpisodes, id: \.self) { episode in
|
||||||
|
EpisodeRowCard(viewModel: viewModel, episode: episode)
|
||||||
|
.id(episode.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.onChange(of: viewModel.selectedSeason) { _ in
|
||||||
|
if viewModel.selectedSeason?.id == viewModel.item.seasonId {
|
||||||
|
reader.scrollTo(viewModel.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.seasonsEpisodes) { _ in
|
||||||
|
if viewModel.selectedSeason?.id == viewModel.item.seasonId {
|
||||||
|
reader.scrollTo(viewModel.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,7 +62,7 @@ struct ItemViewBody: View {
|
||||||
|
|
||||||
// MARK: Genres
|
// MARK: Genres
|
||||||
|
|
||||||
if let genres = viewModel.item.genreItems {
|
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
||||||
PillHStackView(title: L10n.genres,
|
PillHStackView(title: L10n.genres,
|
||||||
items: genres,
|
items: genres,
|
||||||
selectedAction: { genre in
|
selectedAction: { genre in
|
||||||
|
@ -84,7 +84,9 @@ struct ItemViewBody: View {
|
||||||
// MARK: Episodes
|
// MARK: Episodes
|
||||||
|
|
||||||
if let episodeViewModel = viewModel as? EpisodeItemViewModel {
|
if let episodeViewModel = viewModel as? EpisodeItemViewModel {
|
||||||
EpisodesRowView(viewModel: EpisodesRowViewModel(episodeItemViewModel: episodeViewModel))
|
EpisodesRowView(viewModel: episodeViewModel, onlyCurrentSeason: false)
|
||||||
|
} else if let seasonViewModel = viewModel as? SeasonItemViewModel {
|
||||||
|
EpisodesRowView(viewModel: seasonViewModel, onlyCurrentSeason: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Series
|
// MARK: Series
|
||||||
|
@ -133,12 +135,12 @@ struct ItemViewBody: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: More Like This
|
// MARK: Recommended
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitImageHStackView(items: viewModel.similarItems,
|
PortraitImageHStackView(items: viewModel.similarItems,
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.moreLikeThis.text
|
L10n.recommended.text
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
|
@ -63,14 +63,8 @@ struct ItemLandscapeMainView: View {
|
||||||
|
|
||||||
// MARK: ItemViewBody
|
// MARK: ItemViewBody
|
||||||
|
|
||||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
ItemViewBody()
|
||||||
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
.environmentObject(viewModel)
|
||||||
itemRouter.route(to: \.item, episode)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ItemViewBody()
|
|
||||||
.environmentObject(viewModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,19 @@ struct ItemLandscapeTopBarView: View {
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if viewModel.item.itemType.showDetails {
|
// MARK: Details
|
||||||
// MARK: Runtime
|
|
||||||
|
HStack {
|
||||||
|
|
||||||
|
if viewModel.item.unaired {
|
||||||
|
if let premiereDateLabel = viewModel.item.airDateLabel {
|
||||||
|
Text(premiereDateLabel)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let runtime = viewModel.item.getItemRuntime() {
|
if let runtime = viewModel.item.getItemRuntime() {
|
||||||
Text(runtime)
|
Text(runtime)
|
||||||
|
@ -36,11 +47,7 @@ struct ItemLandscapeTopBarView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Details
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
if viewModel.item.productionYear != nil {
|
if viewModel.item.productionYear != nil {
|
||||||
Text(String(viewModel.item.productionYear ?? 0))
|
Text(String(viewModel.item.productionYear ?? 0))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
|
@ -42,6 +42,16 @@ struct PortraitHeaderOverlayView: View {
|
||||||
// MARK: Details
|
// MARK: Details
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
if viewModel.item.unaired {
|
||||||
|
if let premiereDateLabel = viewModel.item.airDateLabel {
|
||||||
|
Text(premiereDateLabel)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.shouldDisplayRuntime() {
|
if viewModel.shouldDisplayRuntime() {
|
||||||
if let runtime = viewModel.item.getItemRuntime() {
|
if let runtime = viewModel.item.getItemRuntime() {
|
||||||
Text(runtime)
|
Text(runtime)
|
||||||
|
|
|
@ -46,16 +46,8 @@ struct ItemPortraitMainView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 70)
|
.frame(height: 70)
|
||||||
|
|
||||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
ItemViewBody()
|
||||||
Spacer()
|
.environmentObject(viewModel)
|
||||||
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
|
||||||
itemRouter.route(to: \.item, episode)
|
|
||||||
}
|
|
||||||
.padding(.top, 5)
|
|
||||||
} else {
|
|
||||||
ItemViewBody()
|
|
||||||
.environmentObject(viewModel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// Swiftfin is subject to the terms of the Mozilla Public
|
||||||
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MissingItemsSettingsView: View {
|
||||||
|
|
||||||
|
@Default(.shouldShowMissingSeasons)
|
||||||
|
var shouldShowMissingSeasons
|
||||||
|
|
||||||
|
@Default(.shouldShowMissingEpisodes)
|
||||||
|
var shouldShowMissingEpisodes
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Toggle("Show Missing Seasons", isOn: $shouldShowMissingSeasons)
|
||||||
|
Toggle("Show Missing Episodes", isOn: $shouldShowMissingEpisodes)
|
||||||
|
} header: {
|
||||||
|
Text("Missing Items")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -141,6 +141,17 @@ struct SettingsView: View {
|
||||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||||
Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
settingsRouter.route(to: \.missingSettings)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Missing Items")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Picker(L10n.appearance, selection: $appAppearance) {
|
Picker(L10n.appearance, selection: $appAppearance) {
|
||||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue