iOS/iPadOS - Landscape/Thumb Posters (#526)
This commit is contained in:
parent
8911ef9ec8
commit
8181db13de
|
@ -24,8 +24,6 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
@Route(.push)
|
||||
var customizeViewsSettings = makeCustomizeViewsSettings
|
||||
@Route(.push)
|
||||
var missingSettings = makeMissingSettings
|
||||
@Route(.push)
|
||||
var about = makeAbout
|
||||
|
||||
#if !os(tvOS)
|
||||
|
@ -37,8 +35,7 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
|
||||
@ViewBuilder
|
||||
func makeServerDetail() -> some View {
|
||||
let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server)
|
||||
ServerDetailView(viewModel: viewModel)
|
||||
ServerDetailView(viewModel: .init(server: SessionManager.main.currentLogin.server))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -56,11 +53,6 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||
CustomizeViewsSettings()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeMissingSettings() -> some View {
|
||||
MissingItemsSettingsView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeAbout() -> some View {
|
||||
AboutAppView()
|
||||
|
|
|
@ -13,7 +13,7 @@ import UIKit
|
|||
|
||||
// MARK: PortraitPoster
|
||||
|
||||
extension BaseItemDto: PortraitPoster {
|
||||
extension BaseItemDto: Poster {
|
||||
|
||||
var title: String {
|
||||
switch type {
|
||||
|
@ -36,7 +36,7 @@ extension BaseItemDto: PortraitPoster {
|
|||
var showTitle: Bool {
|
||||
switch type {
|
||||
case .episode, .series, .movie, .boxSet:
|
||||
return Defaults[.showPosterLabels]
|
||||
return Defaults[.Customization.showPosterLabels]
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
@ -50,20 +50,19 @@ extension BaseItemDto: PortraitPoster {
|
|||
return imageSource(.primary, maxWidth: maxWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: LandscapePoster
|
||||
|
||||
extension BaseItemDto {
|
||||
func landscapePosterImageSources(maxWidth: CGFloat) -> [ImageSource] {
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool = false) -> [ImageSource] {
|
||||
switch type {
|
||||
case .episode:
|
||||
// TODO: Set episode image preference based on defaults
|
||||
return [
|
||||
seriesImageSource(.thumb, maxWidth: maxWidth),
|
||||
seriesImageSource(.backdrop, maxWidth: maxWidth),
|
||||
imageSource(.primary, maxWidth: maxWidth),
|
||||
]
|
||||
if single || Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] {
|
||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||
} else {
|
||||
return [
|
||||
seriesImageSource(.thumb, maxWidth: maxWidth),
|
||||
seriesImageSource(.backdrop, maxWidth: maxWidth),
|
||||
imageSource(.primary, maxWidth: maxWidth),
|
||||
]
|
||||
}
|
||||
default:
|
||||
return [
|
||||
imageSource(.thumb, maxWidth: maxWidth),
|
||||
|
|
|
@ -12,7 +12,7 @@ import UIKit
|
|||
|
||||
// MARK: PortraitImageStackable
|
||||
|
||||
extension BaseItemPerson: PortraitPoster {
|
||||
extension BaseItemPerson: Poster {
|
||||
|
||||
var title: String {
|
||||
self.name ?? "--"
|
||||
|
@ -43,4 +43,8 @@ extension BaseItemPerson: PortraitPoster {
|
|||
|
||||
return ImageSource(url: url, blurHash: blurHash)
|
||||
}
|
||||
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,12 +46,28 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
func poster(type: PosterType, width: CGFloat) -> some View {
|
||||
Group {
|
||||
switch type {
|
||||
case .portrait:
|
||||
self.portraitPoster(width: width)
|
||||
case .landscape:
|
||||
self.landscapePoster(width: width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies Portrait Poster frame with proper corner radius ratio against the width
|
||||
func portraitPoster(width: CGFloat) -> some View {
|
||||
self.frame(width: width, height: width * 1.5)
|
||||
.cornerRadius((width * 1.5) / 40)
|
||||
}
|
||||
|
||||
func landscapePoster(width: CGFloat) -> some View {
|
||||
self.frame(width: width, height: width / 1.77)
|
||||
.cornerRadius(width / 30)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func padding2(_ edges: Edge.Set = .all) -> some View {
|
||||
self.padding(edges)
|
||||
|
@ -74,4 +90,8 @@ extension View {
|
|||
func bottomEdgeGradient(bottomColor: Color) -> some View {
|
||||
self.modifier(BottomEdgeGradientModifier(bottomColor: bottomColor))
|
||||
}
|
||||
|
||||
func posterShadow() -> some View {
|
||||
self.shadow(radius: 4, y: 2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ enum ItemViewType: String, CaseIterable, Defaults.Serializable {
|
|||
case compactLogo
|
||||
case cinematic
|
||||
|
||||
var label: String {
|
||||
var localizedName: String {
|
||||
switch self {
|
||||
case .compactPoster:
|
||||
return L10n.compactPoster
|
||||
|
|
|
@ -14,6 +14,9 @@ protocol Poster: Hashable {
|
|||
var title: String { get }
|
||||
var subtitle: String? { get }
|
||||
var showTitle: Bool { get }
|
||||
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource
|
||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource]
|
||||
}
|
||||
|
||||
extension Poster {
|
||||
|
@ -22,11 +25,3 @@ extension Poster {
|
|||
hasher.combine(subtitle)
|
||||
}
|
||||
}
|
||||
|
||||
protocol PortraitPoster: Poster {
|
||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource
|
||||
}
|
||||
|
||||
protocol LandscapePoster: Poster {
|
||||
func landscapePosterImageSources(maxWidth: CGFloat) -> [ImageSource]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// 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 Foundation
|
||||
|
||||
enum PosterType: String, CaseIterable, Defaults.Serializable {
|
||||
case portrait
|
||||
case landscape
|
||||
|
||||
var localizedName: String {
|
||||
switch self {
|
||||
case .portrait:
|
||||
return "Portrait"
|
||||
case .landscape:
|
||||
return "Landscape"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,96 +10,95 @@ import Defaults
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// TODO: Refactor...
|
||||
// TODO: Organize
|
||||
|
||||
extension SwiftfinStore {
|
||||
enum Defaults {
|
||||
static let generalSuite: UserDefaults = .init(suiteName: "swiftfinstore-general-defaults")!
|
||||
|
||||
static let universalSuite: UserDefaults = .init(suiteName: "swiftfinstore-universal-defaults")!
|
||||
}
|
||||
extension UserDefaults {
|
||||
static let generalSuite = UserDefaults(suiteName: "swiftfinstore-general-defaults")!
|
||||
static let universalSuite = UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
|
||||
}
|
||||
|
||||
extension Defaults.Keys {
|
||||
// Universal settings
|
||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
|
||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: .universalSuite)
|
||||
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: .universalSuite)
|
||||
|
||||
// General settings
|
||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectSubtitlesLangCode = Key<String>(
|
||||
"AutoSelectSubtitlesLangCode",
|
||||
default: "Auto",
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
)
|
||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: .generalSuite)
|
||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: .generalSuite)
|
||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: .generalSuite)
|
||||
|
||||
// Customize settings
|
||||
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let showFlattenView = Key<Bool>("showFlattenView", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let itemViewType = Key<ItemViewType>("itemViewType", default: .compactLogo, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
enum Customization {
|
||||
static let showFlattenView = Key<Bool>("showFlattenView", default: true, suite: .generalSuite)
|
||||
static let itemViewType = Key<ItemViewType>("itemViewType", default: .compactLogo, suite: .generalSuite)
|
||||
|
||||
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: .generalSuite)
|
||||
static let nextUpPosterType = Key<PosterType>("nextUpPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let recommendedPosterType = Key<PosterType>("recommendedPosterType", default: .portrait, suite: .generalSuite)
|
||||
|
||||
enum Episodes {
|
||||
static let useSeriesLandscapeBackdrop = Key<Bool>("useSeriesBackdrop", default: true, suite: .generalSuite)
|
||||
}
|
||||
}
|
||||
|
||||
// Video player / overlay settings
|
||||
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: .generalSuite)
|
||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: .generalSuite)
|
||||
static let systemControlGesturesEnabled = Key<Bool>(
|
||||
"systemControlGesturesEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let playerGesturesLockGestureEnabled = Key<Bool>(
|
||||
"playerGesturesLockGestureEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let seekSlideGestureEnabled = Key<Bool>(
|
||||
"seekSlideGestureEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>(
|
||||
"videoPlayerJumpForward",
|
||||
default: .fifteen,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>(
|
||||
"videoPlayerJumpBackward",
|
||||
default: .fifteen,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: .generalSuite)
|
||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: .generalSuite)
|
||||
static let subtitleFontName = Key<String>(
|
||||
"subtitleFontName",
|
||||
default: UIFont.systemFont(ofSize: 14).fontName,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: .generalSuite)
|
||||
|
||||
// Should show video player items
|
||||
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", 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 shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: .generalSuite)
|
||||
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: .generalSuite)
|
||||
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: .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)
|
||||
static let shouldShowMissingSeasons = Key<Bool>("shouldShowMissingSeasons", default: true, suite: .generalSuite)
|
||||
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: .generalSuite)
|
||||
|
||||
// Should show video player items in overlay menu
|
||||
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>(
|
||||
"shouldShowJumpButtonsInMenu",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
|
||||
static let shouldShowChaptersInfoInBottomOverlay = Key<Bool>(
|
||||
"shouldShowChaptersInfoInBottomOverlay",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
|
||||
// Experimental settings
|
||||
|
@ -107,16 +106,16 @@ extension Defaults.Keys {
|
|||
static let syncSubtitleStateWithAdjacent = Key<Bool>(
|
||||
"experimental.syncSubtitleState",
|
||||
default: false,
|
||||
suite: SwiftfinStore.Defaults.generalSuite
|
||||
suite: .generalSuite
|
||||
)
|
||||
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let liveTVForceDirectPlay = Key<Bool>("liveTVForceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let liveTVNativePlayer = Key<Bool>("liveTVNativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: .generalSuite)
|
||||
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: .generalSuite)
|
||||
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: .generalSuite)
|
||||
static let liveTVForceDirectPlay = Key<Bool>("liveTVForceDirectPlay", default: false, suite: .generalSuite)
|
||||
static let liveTVNativePlayer = Key<Bool>("liveTVNativePlayer", default: false, suite: .generalSuite)
|
||||
}
|
||||
|
||||
// tvos specific
|
||||
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: .generalSuite)
|
||||
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: .generalSuite)
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ final class BasicAppSettingsViewModel: ViewModel {
|
|||
let appearances = AppAppearance.allCases
|
||||
|
||||
func resetUserSettings() {
|
||||
SwiftfinStore.Defaults.generalSuite.removeAll()
|
||||
UserDefaults.generalSuite.removeAll()
|
||||
}
|
||||
|
||||
func resetAppSettings() {
|
||||
SwiftfinStore.Defaults.universalSuite.removeAll()
|
||||
UserDefaults.universalSuite.removeAll()
|
||||
}
|
||||
|
||||
func removeAllUsers() {
|
||||
|
|
|
@ -96,7 +96,7 @@ final class LibraryViewModel: ViewModel {
|
|||
genreIDs = filters.withGenres.compactMap(\.id)
|
||||
}
|
||||
let sortBy = filters.sortBy.map(\.rawValue)
|
||||
let queryRecursive = Defaults[.showFlattenView] || filters.filters.contains(.isFavorite) ||
|
||||
let queryRecursive = Defaults[.Customization.showFlattenView] || filters.filters.contains(.isFavorite) ||
|
||||
self.person != nil ||
|
||||
self.genre != nil ||
|
||||
self.studio != nil
|
||||
|
@ -104,7 +104,7 @@ final class LibraryViewModel: ViewModel {
|
|||
if filters.filters.contains(.isFavorite) {
|
||||
includeItemTypes = [.movie, .series, .season, .episode, .boxSet]
|
||||
} else {
|
||||
includeItemTypes = [.movie, .series, .boxSet] + (Defaults[.showFlattenView] ? [] : [.folder])
|
||||
includeItemTypes = [.movie, .series, .boxSet] + (Defaults[.Customization.showFlattenView] ? [] : [.folder])
|
||||
}
|
||||
|
||||
ItemsAPI.getItemsByUserId(
|
||||
|
|
|
@ -70,8 +70,10 @@ struct ImageView<ImageType: View, PlaceholderView: View, FailureView: View>: Vie
|
|||
_placeholder(currentSource)
|
||||
} else if let _image = state.image {
|
||||
image(_image.resizingMode(resizingMode))
|
||||
} else {
|
||||
failure()
|
||||
} else if state.error != nil {
|
||||
failure().onAppear {
|
||||
sources.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
.pipeline(ImagePipeline(configuration: .withDataCache))
|
||||
|
|
|
@ -1,56 +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 SwiftUI
|
||||
import SwiftUICollection
|
||||
|
||||
struct PortraitButton<Item: PortraitPoster>: View {
|
||||
|
||||
let item: Item
|
||||
let selectedAction: (Item) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: 270))
|
||||
.failure {
|
||||
InitialFailureView(item.title.initials)
|
||||
}
|
||||
.frame(width: 270, height: 405)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if item.showTitle {
|
||||
HStack {
|
||||
Text(item.title)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
.frame(width: 250)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
if let subtitle = item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.zIndex(-1)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.focusSection()
|
||||
}
|
||||
}
|
|
@ -1,89 +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
|
||||
import SwiftUICollection
|
||||
import TVUIKit
|
||||
|
||||
struct PortraitPosterHStack<Item: PortraitPoster, TrailingContent: View>: View {
|
||||
|
||||
private let loading: Bool
|
||||
private let title: String
|
||||
private let items: [Item]
|
||||
private let selectedAction: (Item) -> Void
|
||||
private let trailingContent: () -> TrailingContent
|
||||
|
||||
init(
|
||||
loading: Bool = false,
|
||||
title: String,
|
||||
items: [Item],
|
||||
@ViewBuilder trailingContent: @escaping () -> TrailingContent,
|
||||
selectedAction: @escaping (Item) -> Void
|
||||
) {
|
||||
self.loading = loading
|
||||
self.title = title
|
||||
self.items = items
|
||||
self.trailingContent = trailingContent
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
Text(title)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 50)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
if loading {
|
||||
ForEach(0 ..< 10) { _ in
|
||||
PortraitButton(
|
||||
item: BaseItemDto.placeHolder,
|
||||
selectedAction: { _ in }
|
||||
)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
} else if items.isEmpty {
|
||||
PortraitButton(
|
||||
item: BaseItemDto.noResults,
|
||||
selectedAction: { _ in }
|
||||
)
|
||||
} else {
|
||||
ForEach(items, id: \.hashValue) { item in
|
||||
PortraitButton(item: item) { item in
|
||||
selectedAction(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trailingContent()
|
||||
}
|
||||
.padding(.horizontal, 50)
|
||||
.padding2(.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PortraitPosterHStack where TrailingContent == EmptyView {
|
||||
init(
|
||||
loading: Bool = false,
|
||||
title: String,
|
||||
items: [Item],
|
||||
selectedAction: @escaping (Item) -> Void
|
||||
) {
|
||||
self.loading = loading
|
||||
self.title = title
|
||||
self.items = items
|
||||
self.trailingContent = { EmptyView() }
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
||||
|
||||
private let landscapePosterWidth = 490.0
|
||||
private let portraitPosterWidth = 250.0
|
||||
|
||||
private let item: Item
|
||||
private let type: PosterType
|
||||
private let itemScale: CGFloat
|
||||
private let horizontalAlignment: HorizontalAlignment
|
||||
private let content: (Item) -> Content
|
||||
private let imageOverlay: (Item) -> ImageOverlay
|
||||
private let contextMenu: (Item) -> ContextMenu
|
||||
private let onSelect: (Item) -> Void
|
||||
private let singleImage: Bool
|
||||
|
||||
private var itemWidth: CGFloat {
|
||||
switch type {
|
||||
case .portrait:
|
||||
return portraitPosterWidth * itemScale
|
||||
case .landscape:
|
||||
return landscapePosterWidth * itemScale
|
||||
}
|
||||
}
|
||||
|
||||
private init(
|
||||
item: Item,
|
||||
type: PosterType,
|
||||
itemScale: CGFloat,
|
||||
horizontalAlignment: HorizontalAlignment,
|
||||
@ViewBuilder content: @escaping (Item) -> Content,
|
||||
@ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay,
|
||||
@ViewBuilder contextMenu: @escaping (Item) -> ContextMenu,
|
||||
onSelect: @escaping (Item) -> Void,
|
||||
singleImage: Bool
|
||||
) {
|
||||
self.item = item
|
||||
self.type = type
|
||||
self.itemScale = itemScale
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.content = content
|
||||
self.imageOverlay = imageOverlay
|
||||
self.contextMenu = contextMenu
|
||||
self.onSelect = onSelect
|
||||
self.singleImage = singleImage
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
Button {
|
||||
onSelect(item)
|
||||
} label: {
|
||||
switch type {
|
||||
case .portrait:
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
||||
.poster(type: type, width: itemWidth)
|
||||
case .landscape:
|
||||
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
||||
.poster(type: type, width: itemWidth)
|
||||
}
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.contextMenu(menuItems: {
|
||||
contextMenu(item)
|
||||
})
|
||||
.overlay {
|
||||
imageOverlay(item)
|
||||
.poster(type: type, width: itemWidth)
|
||||
}
|
||||
.posterShadow()
|
||||
|
||||
content(item)
|
||||
.zIndex(-1)
|
||||
}
|
||||
.frame(width: itemWidth)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterButton where Content == PosterButtonDefaultContentView<Item>,
|
||||
ImageOverlay == EmptyView,
|
||||
ContextMenu == EmptyView
|
||||
{
|
||||
init(item: Item, type: PosterType, singleImage: Bool = false) {
|
||||
self.init(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: 1,
|
||||
horizontalAlignment: .leading,
|
||||
content: { PosterButtonDefaultContentView(item: $0) },
|
||||
imageOverlay: { _ in EmptyView() },
|
||||
contextMenu: { _ in EmptyView() },
|
||||
onSelect: { _ in },
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterButton {
|
||||
@ViewBuilder
|
||||
func horizontalAlignment(_ alignment: HorizontalAlignment) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: alignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func scaleItem(_ scale: CGFloat) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: scale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func content<C: View>(@ViewBuilder _ content: @escaping (Item) -> C) -> PosterButton<Item, C, ImageOverlay, ContextMenu> {
|
||||
PosterButton<Item, C, ImageOverlay, ContextMenu>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func imageOverlay<O: View>(@ViewBuilder _ imageOverlay: @escaping (Item) -> O) -> PosterButton<Item, Content, O, ContextMenu> {
|
||||
PosterButton<Item, Content, O, ContextMenu>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contextMenu<M: View>(@ViewBuilder _ contextMenu: @escaping (Item) -> M) -> PosterButton<Item, Content, ImageOverlay, M> {
|
||||
PosterButton<Item, Content, ImageOverlay, M>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func onSelect(_ action: @escaping (Item) -> Void) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: action,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: default content view
|
||||
|
||||
struct PosterButtonDefaultContentView<Item: Poster>: View {
|
||||
|
||||
let item: Item
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let description = item.subtitle {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PosterHStack<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View, TrailingContent: View>: View {
|
||||
|
||||
private let title: String
|
||||
private let type: PosterType
|
||||
private let items: [Item]
|
||||
private let itemScale: CGFloat
|
||||
private let content: (Item) -> Content
|
||||
private let imageOverlay: (Item) -> ImageOverlay
|
||||
private let contextMenu: (Item) -> ContextMenu
|
||||
private let trailingContent: () -> TrailingContent
|
||||
private let onSelect: (Item) -> Void
|
||||
|
||||
private init(
|
||||
title: String,
|
||||
type: PosterType,
|
||||
items: [Item],
|
||||
itemScale: CGFloat,
|
||||
@ViewBuilder content: @escaping (Item) -> Content,
|
||||
@ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay,
|
||||
@ViewBuilder contextMenu: @escaping (Item) -> ContextMenu,
|
||||
@ViewBuilder trailingContent: @escaping () -> TrailingContent,
|
||||
onSelect: @escaping (Item) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.items = items
|
||||
self.itemScale = itemScale
|
||||
self.content = content
|
||||
self.imageOverlay = imageOverlay
|
||||
self.contextMenu = contextMenu
|
||||
self.trailingContent = trailingContent
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.accessibility(addTraits: [.isHeader])
|
||||
.padding(.leading, 50)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top) {
|
||||
ForEach(items, id: \.hashValue) { item in
|
||||
PosterButton(item: item, type: type)
|
||||
.scaleItem(itemScale)
|
||||
.imageOverlay(imageOverlay)
|
||||
.contextMenu(contextMenu)
|
||||
.onSelect(onSelect)
|
||||
}
|
||||
|
||||
trailingContent()
|
||||
}
|
||||
.padding(.horizontal, 50)
|
||||
.padding2(.vertical)
|
||||
}
|
||||
}
|
||||
.focusSection()
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterHStack where Content == PosterButtonDefaultContentView<Item>,
|
||||
ImageOverlay == EmptyView,
|
||||
ContextMenu == EmptyView,
|
||||
TrailingContent == EmptyView
|
||||
{
|
||||
init(
|
||||
title: String,
|
||||
type: PosterType,
|
||||
items: [Item]
|
||||
) {
|
||||
self.init(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: 1,
|
||||
content: { PosterButtonDefaultContentView(item: $0) },
|
||||
imageOverlay: { _ in EmptyView() },
|
||||
contextMenu: { _ in EmptyView() },
|
||||
trailingContent: { EmptyView() },
|
||||
onSelect: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterHStack {
|
||||
@ViewBuilder
|
||||
func scaleItems(_ scale: CGFloat) -> PosterHStack {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: scale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func content<C: View>(@ViewBuilder _ content: @escaping (Item) -> C)
|
||||
-> PosterHStack<Item, C, ImageOverlay, ContextMenu, TrailingContent> {
|
||||
PosterHStack<Item, C, ImageOverlay, ContextMenu, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func imageOverlay<O: View>(@ViewBuilder _ imageOverlay: @escaping (Item) -> O)
|
||||
-> PosterHStack<Item, Content, O, ContextMenu, TrailingContent> {
|
||||
PosterHStack<Item, Content, O, ContextMenu, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contextMenu<M: View>(@ViewBuilder _ contextMenu: @escaping (Item) -> M)
|
||||
-> PosterHStack<Item, Content, ImageOverlay, M, TrailingContent> {
|
||||
PosterHStack<Item, Content, ImageOverlay, M, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func trailing<T: View>(@ViewBuilder _ trailingContent: @escaping () -> T)
|
||||
-> PosterHStack<Item, Content, ImageOverlay, ContextMenu, T> {
|
||||
PosterHStack<Item, Content, ImageOverlay, ContextMenu, T>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func onSelect(_ onSelect: @escaping (Item) -> Void) -> PosterHStack {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
}
|
|
@ -35,12 +35,10 @@ struct HomeView: View {
|
|||
)
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.nextUp,
|
||||
items: viewModel.nextUpItems
|
||||
) { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HomeCinematicView(
|
||||
|
@ -49,21 +47,17 @@ struct HomeView: View {
|
|||
)
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.nextUp,
|
||||
items: viewModel.nextUpItems
|
||||
) { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.latestAddedItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recentlyAdded,
|
||||
items: viewModel.latestAddedItems
|
||||
) { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recentlyAdded, type: .portrait, items: viewModel.latestAddedItems)
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,13 +53,11 @@ extension CollectionItemView {
|
|||
.padding(.top, 5)
|
||||
}
|
||||
|
||||
PortraitPosterHStack(
|
||||
title: L10n.items,
|
||||
items: viewModel.collectionItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "items", top: "mediaButtons", bottom: "about")
|
||||
PosterHStack(title: L10n.items, type: .portrait, items: viewModel.collectionItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "items", top: "mediaButtons", bottom: "about")
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
.focusGuide(focusGuide, tag: "about", top: "items")
|
||||
|
|
|
@ -45,13 +45,11 @@ extension EpisodeItemView {
|
|||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about")
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about")
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
.focusGuide(focusGuide, tag: "about", top: "recommended")
|
||||
|
|
|
@ -53,13 +53,11 @@ extension MovieItemView {
|
|||
.padding(.top, 5)
|
||||
}
|
||||
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about")
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "mediaButtons", bottom: "about")
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
.focusGuide(focusGuide, tag: "about", top: "recommended")
|
||||
|
|
|
@ -62,13 +62,11 @@ extension SeriesItemView {
|
|||
.frame(height: 0.5)
|
||||
.id("seasonsRecommendedContentDivider")
|
||||
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "seasons", bottom: "about")
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
.focusGuide(focusGuide, tag: "recommended", top: "seasons", bottom: "about")
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
.focusGuide(focusGuide, tag: "about", top: "recommended")
|
||||
|
|
|
@ -17,40 +17,39 @@ struct LatestInLibraryView: View {
|
|||
var viewModel: LatestMediaViewModel
|
||||
|
||||
var body: some View {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.latestWithString(viewModel.library.displayName),
|
||||
items: viewModel.items
|
||||
) {
|
||||
Button {
|
||||
router.route(to: \.library, (
|
||||
viewModel: .init(
|
||||
parentID: viewModel.library.id!,
|
||||
filters: LibraryFilters(
|
||||
filters: [],
|
||||
sortOrder: [.descending],
|
||||
sortBy: [.dateAdded]
|
||||
)
|
||||
),
|
||||
title: viewModel.library.displayName
|
||||
))
|
||||
} label: {
|
||||
ZStack {
|
||||
Color(UIColor.darkGray)
|
||||
.opacity(0.5)
|
||||
PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: .portrait, items: viewModel.items)
|
||||
.trailing {
|
||||
Button {
|
||||
router.route(to: \.library, (
|
||||
viewModel: .init(
|
||||
parentID: viewModel.library.id!,
|
||||
filters: LibraryFilters(
|
||||
filters: [],
|
||||
sortOrder: [.descending],
|
||||
sortBy: [.dateAdded]
|
||||
)
|
||||
),
|
||||
title: viewModel.library.displayName
|
||||
))
|
||||
} label: {
|
||||
ZStack {
|
||||
Color(UIColor.darkGray)
|
||||
.opacity(0.5)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.title)
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.title)
|
||||
|
||||
L10n.seeAll.text
|
||||
.font(.title3)
|
||||
L10n.seeAll.text
|
||||
.font(.title3)
|
||||
}
|
||||
}
|
||||
.poster(type: .portrait, width: 250)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
.frame(width: 257, height: 380)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
} selectedAction: { item in
|
||||
router.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,9 @@ import SwiftUI
|
|||
|
||||
struct CustomizeViewsSettings: View {
|
||||
|
||||
@Default(.showPosterLabels)
|
||||
@Default(.Customization.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
@Default(.showFlattenView)
|
||||
@Default(.Customization.showFlattenView)
|
||||
var showFlattenView
|
||||
|
||||
var body: some View {
|
||||
|
@ -24,8 +22,6 @@ struct CustomizeViewsSettings: View {
|
|||
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
|
||||
// TODO: Uncomment when cast and crew implemented in item views
|
||||
// Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||
Toggle(L10n.showFlattenView, isOn: $showFlattenView)
|
||||
|
||||
} header: {
|
||||
|
|
|
@ -18,8 +18,6 @@ struct SettingsView: View {
|
|||
@ObservedObject
|
||||
var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.autoSelectAudioLangCode)
|
||||
var autoSelectAudioLangcode
|
||||
@Default(.videoPlayerJumpForward)
|
||||
var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward)
|
||||
|
@ -28,8 +26,6 @@ struct SettingsView: View {
|
|||
var downActionShowsMenu
|
||||
@Default(.confirmClose)
|
||||
var confirmClose
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.resumeOffset)
|
||||
var resumeOffset
|
||||
@Default(.subtitleSize)
|
||||
|
@ -134,17 +130,6 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.missingSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.missingItems.text
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Picker(L10n.subtitleSize, selection: $subtitleSize) {
|
||||
ForEach(SubtitleSize.allCases, id: \.self) { size in
|
||||
Text(size.label).tag(size.rawValue)
|
||||
|
|
|
@ -87,7 +87,6 @@
|
|||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||
53F866442687A45F00DCD1D7 /* PortraitPosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */; };
|
||||
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; };
|
||||
|
@ -318,15 +317,14 @@
|
|||
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; };
|
||||
E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; };
|
||||
E168BD11289A4162001A6922 /* HomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD09289A4162001A6922 /* HomeContentView.swift */; };
|
||||
E168BD12289A4162001A6922 /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */; };
|
||||
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */; };
|
||||
E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; };
|
||||
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0F289A4162001A6922 /* HomeErrorView.swift */; };
|
||||
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16AA60728A364A6009A983C /* PosterButton.swift */; };
|
||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; };
|
||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
|
||||
E176DE6D278E30D2001EFD8D /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */; };
|
||||
E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */; };
|
||||
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; };
|
||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859A2780F1F40094FBCF /* tvOSSlider.swift */; };
|
||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
|
||||
|
@ -341,7 +339,6 @@
|
|||
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; };
|
||||
E18CE0B528A22EDD0092E7F1 /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */; };
|
||||
E18CE0B928A2322D0092E7F1 /* QuickConnectCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */; };
|
||||
E18E01A9288746AF0022598C /* PortraitPosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */; };
|
||||
E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A4288746AF0022598C /* RefreshableScrollView.swift */; };
|
||||
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; };
|
||||
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A7288746AF0022598C /* DotHStack.swift */; };
|
||||
|
@ -465,9 +462,12 @@
|
|||
E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926082887565C002A7A66 /* FocusGuide.swift */; };
|
||||
E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C926092887565C002A7A66 /* EpisodeCard.swift */; };
|
||||
E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C9260A2887565C002A7A66 /* SeriesItemView.swift */; };
|
||||
E1C9261A288756BD002A7A66 /* PortraitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92617288756BD002A7A66 /* PortraitButton.swift */; };
|
||||
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92617288756BD002A7A66 /* PosterButton.swift */; };
|
||||
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92618288756BD002A7A66 /* DotHStack.swift */; };
|
||||
E1C9261C288756BD002A7A66 /* PortraitPosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */; };
|
||||
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92619288756BD002A7A66 /* PosterHStack.swift */; };
|
||||
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; };
|
||||
E1CCF12F28ABF989006CAC9E /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; };
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; };
|
||||
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */; };
|
||||
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; };
|
||||
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */; };
|
||||
|
@ -623,7 +623,6 @@
|
|||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
||||
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitPosterButton.swift; sourceTree = "<group>"; };
|
||||
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = "<group>"; };
|
||||
5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = "<group>"; };
|
||||
5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = "<group>"; };
|
||||
|
@ -781,15 +780,14 @@
|
|||
E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = "<group>"; };
|
||||
E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
E168BD09289A4162001A6922 /* HomeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = "<group>"; };
|
||||
E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = "<group>"; };
|
||||
E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||
E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestInLibraryView.swift; sourceTree = "<group>"; };
|
||||
E168BD0F289A4162001A6922 /* HomeErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
|
||||
E16AA60728A364A6009A983C /* PosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterButton.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>"; };
|
||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = "<group>"; };
|
||||
E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCard.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>"; };
|
||||
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>"; };
|
||||
|
@ -800,7 +798,6 @@
|
|||
E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDtoExtensions.swift; sourceTree = "<group>"; };
|
||||
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = "<group>"; };
|
||||
E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectCoordinator.swift; sourceTree = "<group>"; };
|
||||
E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitPosterHStack.swift; sourceTree = "<group>"; };
|
||||
E18E01A4288746AF0022598C /* RefreshableScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = "<group>"; };
|
||||
E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = "<group>"; };
|
||||
E18E01A7288746AF0022598C /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = "<group>"; };
|
||||
|
@ -888,9 +885,11 @@
|
|||
E1C926082887565C002A7A66 /* FocusGuide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusGuide.swift; sourceTree = "<group>"; };
|
||||
E1C926092887565C002A7A66 /* EpisodeCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCard.swift; sourceTree = "<group>"; };
|
||||
E1C9260A2887565C002A7A66 /* SeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
|
||||
E1C92617288756BD002A7A66 /* PortraitButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitButton.swift; sourceTree = "<group>"; };
|
||||
E1C92617288756BD002A7A66 /* PosterButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PosterButton.swift; sourceTree = "<group>"; };
|
||||
E1C92618288756BD002A7A66 /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = "<group>"; };
|
||||
E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortraitPosterHStack.swift; sourceTree = "<group>"; };
|
||||
E1C92619288756BD002A7A66 /* PosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
|
||||
E1CCF12D28ABF989006CAC9E /* PosterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterType.swift; sourceTree = "<group>"; };
|
||||
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
|
||||
E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1160,6 +1159,7 @@
|
|||
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
|
||||
E1937A60288F32DB00CB80AA /* Poster.swift */,
|
||||
E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
|
||||
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
|
||||
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */,
|
||||
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
||||
|
@ -1176,8 +1176,8 @@
|
|||
E103A6A1278A7EB500820EC7 /* HomeCinematicView */,
|
||||
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
|
||||
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
|
||||
E1C92617288756BD002A7A66 /* PortraitButton.swift */,
|
||||
E1C92619288756BD002A7A66 /* PortraitPosterHStack.swift */,
|
||||
E1C92617288756BD002A7A66 /* PosterButton.swift */,
|
||||
E1C92619288756BD002A7A66 /* PosterHStack.swift */,
|
||||
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
|
||||
E17885A3278105170094FBCF /* SFSymbolButton.swift */,
|
||||
);
|
||||
|
@ -1412,8 +1412,8 @@
|
|||
E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */,
|
||||
E18E01A7288746AF0022598C /* DotHStack.swift */,
|
||||
E18E01A5288746AF0022598C /* PillHStack.swift */,
|
||||
53F866432687A45F00DCD1D7 /* PortraitPosterButton.swift */,
|
||||
E18E01A3288746AF0022598C /* PortraitPosterHStack.swift */,
|
||||
E16AA60728A364A6009A983C /* PosterButton.swift */,
|
||||
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */,
|
||||
E1AA331C2782541500F6439C /* PrimaryButton.swift */,
|
||||
E18E01A4288746AF0022598C /* RefreshableScrollView.swift */,
|
||||
);
|
||||
|
@ -1749,21 +1749,12 @@
|
|||
E168BD0A289A4162001A6922 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E168BD0B289A4162001A6922 /* ContinueWatchingView */,
|
||||
E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */,
|
||||
E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E168BD0B289A4162001A6922 /* ContinueWatchingView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E168BD0C289A4162001A6922 /* ContinueWatchingCard.swift */,
|
||||
E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */,
|
||||
);
|
||||
path = ContinueWatchingView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E176DE6E278E3522001EFD8D /* EpisodesRowView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2094,7 +2085,6 @@
|
|||
children = (
|
||||
E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */,
|
||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
||||
E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */,
|
||||
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */,
|
||||
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
|
||||
6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */,
|
||||
|
@ -2433,10 +2423,10 @@
|
|||
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
|
||||
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||
E1C9261C288756BD002A7A66 /* PortraitPosterHStack.swift in Sources */,
|
||||
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */,
|
||||
C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */,
|
||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */,
|
||||
E1C9261A288756BD002A7A66 /* PortraitButton.swift in Sources */,
|
||||
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
|
||||
E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||
E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */,
|
||||
E1EF473A289A0F610034046B /* TruncatedTextView.swift in Sources */,
|
||||
|
@ -2496,6 +2486,7 @@
|
|||
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
E1CCF12F28ABF989006CAC9E /* PosterType.swift in Sources */,
|
||||
E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */,
|
||||
E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */,
|
||||
E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */,
|
||||
|
@ -2598,12 +2589,12 @@
|
|||
E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */,
|
||||
E18E0208288749200022598C /* BlurView.swift in Sources */,
|
||||
E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitPosterButton.swift in Sources */,
|
||||
E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */,
|
||||
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
|
||||
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */,
|
||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||
E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */,
|
||||
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */,
|
||||
E18CE0AF28A222240092E7F1 /* PublicUserSignInView.swift in Sources */,
|
||||
E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */,
|
||||
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
||||
|
@ -2639,7 +2630,6 @@
|
|||
E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */,
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
|
||||
E18E01A9288746AF0022598C /* PortraitPosterHStack.swift in Sources */,
|
||||
E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */,
|
||||
E18E0204288749200022598C /* Divider.swift in Sources */,
|
||||
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
||||
|
@ -2715,6 +2705,7 @@
|
|||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */,
|
||||
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */,
|
||||
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */,
|
||||
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||
|
@ -2737,7 +2728,6 @@
|
|||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */,
|
||||
E176DE70278E369F001EFD8D /* MissingItemsSettingsView.swift in Sources */,
|
||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
|
||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */,
|
||||
|
@ -2745,6 +2735,7 @@
|
|||
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */,
|
||||
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */,
|
||||
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
|
@ -2767,7 +2758,6 @@
|
|||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
E18E01EA288747230022598C /* MovieItemView.swift in Sources */,
|
||||
E168BD12289A4162001A6922 /* ContinueWatchingCard.swift in Sources */,
|
||||
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||
E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */,
|
||||
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
||||
|
|
|
@ -12,7 +12,17 @@ struct PillHStack<Item: PillStackable>: View {
|
|||
|
||||
let title: String
|
||||
let items: [Item]
|
||||
let selectedAction: (Item) -> Void
|
||||
let onSelect: (Item) -> Void
|
||||
|
||||
private init(
|
||||
title: String,
|
||||
items: [Item],
|
||||
onSelect: @escaping (Item) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.items = items
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -29,21 +39,17 @@ struct PillHStack<Item: PillStackable>: View {
|
|||
HStack {
|
||||
ForEach(items, id: \.title) { item in
|
||||
Button {
|
||||
selectedAction(item)
|
||||
onSelect(item)
|
||||
} label: {
|
||||
ZStack {
|
||||
Color(UIColor.systemFill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.cornerRadius(10)
|
||||
|
||||
Text(item.title)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.fixedSize()
|
||||
.padding(10)
|
||||
}
|
||||
.fixedSize()
|
||||
Text(item.title)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.padding(10)
|
||||
.background {
|
||||
Color.systemFill
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,3 +61,19 @@ struct PillHStack<Item: PillStackable>: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PillHStack {
|
||||
|
||||
init(title: String, items: [Item]) {
|
||||
self.init(title: title, items: items, onSelect: { _ in })
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func onSelect(_ onSelect: @escaping (Item) -> Void) -> PillHStack {
|
||||
PillHStack(
|
||||
title: title,
|
||||
items: items,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +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 SwiftUI
|
||||
|
||||
struct PortraitPosterButton<Item: PortraitPoster>: View {
|
||||
|
||||
@Environment(\.colorScheme)
|
||||
private var colorScheme
|
||||
|
||||
let item: Item
|
||||
let maxWidth: CGFloat
|
||||
let horizontalAlignment: HorizontalAlignment
|
||||
let textAlignment: TextAlignment
|
||||
let selectedAction: (Item) -> Void
|
||||
|
||||
init(
|
||||
item: Item,
|
||||
maxWidth: CGFloat = 110,
|
||||
horizontalAlignment: HorizontalAlignment = .leading,
|
||||
textAlignment: TextAlignment = .leading,
|
||||
selectedAction: @escaping (Item) -> Void
|
||||
) {
|
||||
self.item = item
|
||||
self.maxWidth = maxWidth
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.textAlignment = textAlignment
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: maxWidth))
|
||||
.failure {
|
||||
InitialFailureView(item.title.initials)
|
||||
}
|
||||
.portraitPoster(width: maxWidth)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let description = item.subtitle {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.frame(width: maxWidth)
|
||||
}
|
||||
.if(colorScheme == .light) { view in
|
||||
view.shadow(radius: 4, y: 2)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,84 +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 SwiftUI
|
||||
|
||||
struct PortraitPosterHStack<Item: PortraitPoster, TrailingContent: View>: View {
|
||||
|
||||
private let title: String
|
||||
private let items: [Item]
|
||||
private let itemWidth: CGFloat
|
||||
private let trailingContent: () -> TrailingContent
|
||||
private let selectedAction: (Item) -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
items: [Item],
|
||||
itemWidth: CGFloat = 110,
|
||||
@ViewBuilder trailingContent: @escaping () -> TrailingContent,
|
||||
selectedAction: @escaping (Item) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.items = items
|
||||
self.itemWidth = itemWidth
|
||||
self.trailingContent = trailingContent
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.accessibility(addTraits: [.isHeader])
|
||||
.padding(.leading)
|
||||
.if(UIDevice.isIPad) { view in
|
||||
view.padding(.leading)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
trailingContent()
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 15) {
|
||||
ForEach(items, id: \.hashValue) { item in
|
||||
PortraitPosterButton(
|
||||
item: item,
|
||||
maxWidth: itemWidth,
|
||||
horizontalAlignment: .leading
|
||||
) { item in
|
||||
selectedAction(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isIPad) { view in
|
||||
view.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PortraitPosterHStack where TrailingContent == EmptyView {
|
||||
init(
|
||||
title: String,
|
||||
items: [Item],
|
||||
itemWidth: CGFloat = 110,
|
||||
selectedAction: @escaping (Item) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.items = items
|
||||
self.itemWidth = itemWidth
|
||||
self.trailingContent = { EmptyView() }
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
|
||||
|
||||
@ScaledMetric(relativeTo: .largeTitle)
|
||||
private var landscapePosterWidth = 200.0
|
||||
|
||||
@ScaledMetric(relativeTo: .largeTitle)
|
||||
private var portraitPosterWidth = 100.0
|
||||
|
||||
private let item: Item
|
||||
private let type: PosterType
|
||||
private let itemScale: CGFloat
|
||||
private let horizontalAlignment: HorizontalAlignment
|
||||
private let content: (Item) -> Content
|
||||
private let imageOverlay: (Item) -> ImageOverlay
|
||||
private let contextMenu: (Item) -> ContextMenu
|
||||
private let onSelect: (Item) -> Void
|
||||
private let singleImage: Bool
|
||||
|
||||
private var itemWidth: CGFloat {
|
||||
switch type {
|
||||
case .portrait:
|
||||
return portraitPosterWidth * itemScale
|
||||
case .landscape:
|
||||
return landscapePosterWidth * itemScale
|
||||
}
|
||||
}
|
||||
|
||||
private init(
|
||||
item: Item,
|
||||
type: PosterType,
|
||||
itemScale: CGFloat,
|
||||
horizontalAlignment: HorizontalAlignment,
|
||||
@ViewBuilder content: @escaping (Item) -> Content,
|
||||
@ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay,
|
||||
@ViewBuilder contextMenu: @escaping (Item) -> ContextMenu,
|
||||
onSelect: @escaping (Item) -> Void,
|
||||
singleImage: Bool
|
||||
) {
|
||||
self.item = item
|
||||
self.type = type
|
||||
self.itemScale = itemScale
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.content = content
|
||||
self.imageOverlay = imageOverlay
|
||||
self.contextMenu = contextMenu
|
||||
self.onSelect = onSelect
|
||||
self.singleImage = singleImage
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
Button {
|
||||
onSelect(item)
|
||||
} label: {
|
||||
switch type {
|
||||
case .portrait:
|
||||
ImageView(item.portraitPosterImageSource(maxWidth: itemWidth))
|
||||
case .landscape:
|
||||
ImageView(item.landscapePosterImageSources(maxWidth: itemWidth, single: singleImage))
|
||||
}
|
||||
}
|
||||
.contextMenu(menuItems: {
|
||||
contextMenu(item)
|
||||
})
|
||||
.poster(type: type, width: itemWidth)
|
||||
.overlay {
|
||||
imageOverlay(item)
|
||||
.poster(type: type, width: itemWidth)
|
||||
}
|
||||
.posterShadow()
|
||||
|
||||
content(item)
|
||||
}
|
||||
.frame(width: itemWidth)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterButton where Content == PosterButtonDefaultContentView<Item>,
|
||||
ImageOverlay == EmptyView,
|
||||
ContextMenu == EmptyView
|
||||
{
|
||||
init(item: Item, type: PosterType, singleImage: Bool = false) {
|
||||
self.init(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: 1,
|
||||
horizontalAlignment: .leading,
|
||||
content: { PosterButtonDefaultContentView(item: $0) },
|
||||
imageOverlay: { _ in EmptyView() },
|
||||
contextMenu: { _ in EmptyView() },
|
||||
onSelect: { _ in },
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterButton {
|
||||
@ViewBuilder
|
||||
func horizontalAlignment(_ alignment: HorizontalAlignment) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: alignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func scaleItem(_ scale: CGFloat) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: scale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func content<C: View>(@ViewBuilder _ content: @escaping (Item) -> C) -> PosterButton<Item, C, ImageOverlay, ContextMenu> {
|
||||
PosterButton<Item, C, ImageOverlay, ContextMenu>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func imageOverlay<O: View>(@ViewBuilder _ imageOverlay: @escaping (Item) -> O) -> PosterButton<Item, Content, O, ContextMenu> {
|
||||
PosterButton<Item, Content, O, ContextMenu>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contextMenu<M: View>(@ViewBuilder _ contextMenu: @escaping (Item) -> M) -> PosterButton<Item, Content, ImageOverlay, M> {
|
||||
PosterButton<Item, Content, ImageOverlay, M>(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: onSelect,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func onSelect(_ action: @escaping (Item) -> Void) -> PosterButton {
|
||||
PosterButton(
|
||||
item: item,
|
||||
type: type,
|
||||
itemScale: itemScale,
|
||||
horizontalAlignment: horizontalAlignment,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
onSelect: action,
|
||||
singleImage: singleImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: default content view
|
||||
|
||||
struct PosterButtonDefaultContentView<Item: Poster>: View {
|
||||
|
||||
let item: Item
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let description = item.subtitle {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct PosterHStack<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View, TrailingContent: View>: View {
|
||||
|
||||
private let title: String
|
||||
private let type: PosterType
|
||||
private let items: [Item]
|
||||
private let itemScale: CGFloat
|
||||
private let content: (Item) -> Content
|
||||
private let imageOverlay: (Item) -> ImageOverlay
|
||||
private let contextMenu: (Item) -> ContextMenu
|
||||
private let trailingContent: () -> TrailingContent
|
||||
private let onSelect: (Item) -> Void
|
||||
|
||||
private init(
|
||||
title: String,
|
||||
type: PosterType,
|
||||
items: [Item],
|
||||
itemScale: CGFloat,
|
||||
@ViewBuilder content: @escaping (Item) -> Content,
|
||||
@ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay,
|
||||
@ViewBuilder contextMenu: @escaping (Item) -> ContextMenu,
|
||||
@ViewBuilder trailingContent: @escaping () -> TrailingContent,
|
||||
onSelect: @escaping (Item) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.items = items
|
||||
self.itemScale = itemScale
|
||||
self.content = content
|
||||
self.imageOverlay = imageOverlay
|
||||
self.contextMenu = contextMenu
|
||||
self.trailingContent = trailingContent
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.accessibility(addTraits: [.isHeader])
|
||||
|
||||
Spacer()
|
||||
|
||||
trailingContent()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isIPad) { view in
|
||||
view.padding(.horizontal)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 15) {
|
||||
ForEach(items, id: \.hashValue) { item in
|
||||
PosterButton(item: item, type: type)
|
||||
.scaleItem(itemScale)
|
||||
.imageOverlay(imageOverlay)
|
||||
.contextMenu(contextMenu)
|
||||
.onSelect(onSelect)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isIPad) { view in
|
||||
view.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterHStack where Content == PosterButtonDefaultContentView<Item>,
|
||||
ImageOverlay == EmptyView,
|
||||
ContextMenu == EmptyView,
|
||||
TrailingContent == EmptyView
|
||||
{
|
||||
init(
|
||||
title: String,
|
||||
type: PosterType,
|
||||
items: [Item]
|
||||
) {
|
||||
self.init(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: 1,
|
||||
content: { PosterButtonDefaultContentView(item: $0) },
|
||||
imageOverlay: { _ in EmptyView() },
|
||||
contextMenu: { _ in EmptyView() },
|
||||
trailingContent: { EmptyView() },
|
||||
onSelect: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension PosterHStack {
|
||||
@ViewBuilder
|
||||
func scaleItems(_ scale: CGFloat) -> PosterHStack {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: scale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func content<C: View>(@ViewBuilder _ content: @escaping (Item) -> C)
|
||||
-> PosterHStack<Item, C, ImageOverlay, ContextMenu, TrailingContent> {
|
||||
PosterHStack<Item, C, ImageOverlay, ContextMenu, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func imageOverlay<O: View>(@ViewBuilder _ imageOverlay: @escaping (Item) -> O)
|
||||
-> PosterHStack<Item, Content, O, ContextMenu, TrailingContent> {
|
||||
PosterHStack<Item, Content, O, ContextMenu, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contextMenu<M: View>(@ViewBuilder _ contextMenu: @escaping (Item) -> M)
|
||||
-> PosterHStack<Item, Content, ImageOverlay, M, TrailingContent> {
|
||||
PosterHStack<Item, Content, ImageOverlay, M, TrailingContent>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func trailing<T: View>(@ViewBuilder _ trailingContent: @escaping () -> T)
|
||||
-> PosterHStack<Item, Content, ImageOverlay, ContextMenu, T> {
|
||||
PosterHStack<Item, Content, ImageOverlay, ContextMenu, T>(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func onSelect(_ onSelect: @escaping (Item) -> Void) -> PosterHStack {
|
||||
PosterHStack(
|
||||
title: title,
|
||||
type: type,
|
||||
items: items,
|
||||
itemScale: itemScale,
|
||||
content: content,
|
||||
imageOverlay: imageOverlay,
|
||||
contextMenu: contextMenu,
|
||||
trailingContent: trailingContent,
|
||||
onSelect: onSelect
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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 ContinueWatchingView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var homeRouter: HomeCoordinator.Router
|
||||
@ObservedObject
|
||||
var viewModel: HomeViewModel
|
||||
|
||||
var body: some View {
|
||||
PosterHStack(title: "", type: .landscape, items: viewModel.resumeItems)
|
||||
.scaleItems(1.5)
|
||||
.onSelect { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
.contextMenu { item in
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeItemFromResume(item)
|
||||
} label: {
|
||||
Label(L10n.removeFromResume, systemImage: "minus.circle")
|
||||
}
|
||||
}
|
||||
.imageOverlay { item in
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
|
||||
LinearGradient(
|
||||
colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: 35)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.getItemProgressString() ?? L10n.continue)
|
||||
.font(.subheadline)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.leading, 10)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Color.jellyfinPurple
|
||||
.frame(width: 320 * (item.userData?.playedPercentage ?? 0) / 100, height: 7)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +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 ContinueWatchingLandscapeButton: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var homeRouter: HomeCoordinator.Router
|
||||
|
||||
let item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
ZStack {
|
||||
Group {
|
||||
if item.type == .episode {
|
||||
ImageView([
|
||||
item.seriesImageSource(.thumb, maxWidth: 320),
|
||||
item.seriesImageSource(.backdrop, maxWidth: 320),
|
||||
])
|
||||
.frame(width: 320, height: 180)
|
||||
} else {
|
||||
ImageView([
|
||||
item.imageSource(.thumb, maxWidth: 320),
|
||||
item.imageSource(.backdrop, maxWidth: 320),
|
||||
])
|
||||
.frame(width: 320, height: 180)
|
||||
}
|
||||
}
|
||||
.accessibilityIgnoresInvertColors()
|
||||
|
||||
HStack {
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
|
||||
LinearGradient(
|
||||
colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: 35)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.getItemProgressString() ?? L10n.continue)
|
||||
.font(.subheadline)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.leading, 10)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack {
|
||||
Color.jellyfinPurple
|
||||
.frame(width: 320 * (item.userData?.playedPercentage ?? 0) / 100, height: 7)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 320, height: 180)
|
||||
.mask(Rectangle().cornerRadius(10))
|
||||
.shadow(radius: 4, y: 2)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("\(item.seriesName ?? item.name ?? "")")
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
if item.type == .episode {
|
||||
Text(item.seasonEpisodeLocator ?? "")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +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 ContinueWatchingView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var homeRouter: HomeCoordinator.Router
|
||||
@ObservedObject
|
||||
var viewModel: HomeViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
ForEach(viewModel.resumeItems, id: \.id) { item in
|
||||
ContinueWatchingLandscapeButton(item: item)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeItemFromResume(item)
|
||||
} label: {
|
||||
Label(L10n.removeFromResume, systemImage: "minus.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
|
@ -16,24 +17,25 @@ struct LatestInLibraryView: View {
|
|||
@ObservedObject
|
||||
var viewModel: LatestMediaViewModel
|
||||
|
||||
@Default(.Customization.latestInLibraryPosterType)
|
||||
var latestInLibraryPosterType
|
||||
|
||||
var body: some View {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.latestWithString(viewModel.library.displayName),
|
||||
items: viewModel.items,
|
||||
itemWidth: UIDevice.isIPad ? 130 : 110
|
||||
) {
|
||||
Button {
|
||||
let libraryViewModel = LibraryViewModel(parentID: viewModel.library.id, filters: HomeViewModel.recentFilterSet)
|
||||
homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName))
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.seeAll.text
|
||||
Image(systemName: "chevron.right")
|
||||
PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items)
|
||||
.trailing {
|
||||
Button {
|
||||
let libraryViewModel = LibraryViewModel(parentID: viewModel.library.id, filters: HomeViewModel.recentFilterSet)
|
||||
homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName))
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.seeAll.text
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
.font(.subheadline.bold())
|
||||
}
|
||||
.font(.subheadline.bold())
|
||||
}
|
||||
} selectedAction: { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
.onSelect { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
extension HomeView {
|
||||
|
@ -17,6 +18,11 @@ extension HomeView {
|
|||
@ObservedObject
|
||||
var viewModel: HomeViewModel
|
||||
|
||||
@Default(.Customization.nextUpPosterType)
|
||||
var nextUpPosterType
|
||||
@Default(.Customization.recentlyAddedPosterType)
|
||||
var recentlyAddedPosterType
|
||||
|
||||
var body: some View {
|
||||
RefreshableScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
@ -25,23 +31,17 @@ extension HomeView {
|
|||
}
|
||||
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.nextUp,
|
||||
items: viewModel.nextUpItems,
|
||||
itemWidth: UIDevice.isIPad ? 130 : 110
|
||||
) { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.nextUp, type: nextUpPosterType, items: viewModel.nextUpItems)
|
||||
.onSelect { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.latestAddedItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recentlyAdded,
|
||||
items: viewModel.latestAddedItems,
|
||||
itemWidth: UIDevice.isIPad ? 130 : 110
|
||||
) { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recentlyAdded, type: recentlyAddedPosterType, items: viewModel.latestAddedItems)
|
||||
.onSelect { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
|
|
|
@ -15,23 +15,26 @@ struct EpisodeCard: View {
|
|||
private var itemRouter: ItemCoordinator.Router
|
||||
@ScaledMetric
|
||||
private var staticOverviewHeight: CGFloat = 50
|
||||
@Environment(\.colorScheme)
|
||||
private var colorScheme
|
||||
|
||||
let episode: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if episode != .placeHolder && episode != .noResults {
|
||||
itemRouter.route(to: \.item, episode)
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(episode.imageSource(.primary, maxWidth: 200))
|
||||
.frame(width: 200, height: 112)
|
||||
.cornerRadius(10)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
PosterButton(item: episode, type: .landscape, singleImage: true)
|
||||
.scaleItem(1.2)
|
||||
.imageOverlay { _ in
|
||||
if episode.userData?.played ?? false {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Color.clear
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30, alignment: .bottomTrailing)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.content { _ in
|
||||
VStack(alignment: .leading) {
|
||||
Text(episode.episodeLocator ?? L10n.unknown)
|
||||
.font(.footnote)
|
||||
|
@ -58,11 +61,8 @@ struct EpisodeCard: View {
|
|||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
.frame(width: 200)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.if(colorScheme == .light) { view in
|
||||
view.shadow(radius: 4, y: 2)
|
||||
}
|
||||
.onSelect { _ in
|
||||
itemRouter.route(to: \.item, episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ extension CollectionItemView {
|
|||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ extension CollectionItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -49,12 +49,10 @@ extension CollectionItemView {
|
|||
// MARK: Items
|
||||
|
||||
if !viewModel.collectionItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.items,
|
||||
items: viewModel.collectionItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.items, type: .portrait, items: viewModel.collectionItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ struct CollectionItemView: View {
|
|||
|
||||
@ObservedObject
|
||||
var viewModel: CollectionItemViewModel
|
||||
@Default(.itemViewType)
|
||||
@Default(.Customization.itemViewType)
|
||||
private var itemViewType
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -47,11 +47,10 @@ extension EpisodeItemView {
|
|||
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
||||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres,
|
||||
selectedAction: { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
)
|
||||
items: genres
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -60,7 +59,7 @@ extension EpisodeItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -70,12 +69,10 @@ extension EpisodeItemView {
|
|||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
|
||||
!castAndCrew.isEmpty
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ extension MovieItemView {
|
|||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ extension MovieItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -53,12 +53,10 @@ extension MovieItemView {
|
|||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
|
||||
!castAndCrew.isEmpty
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -66,12 +64,10 @@ extension MovieItemView {
|
|||
// MARK: Similar
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ struct MovieItemView: View {
|
|||
|
||||
@ObservedObject
|
||||
var viewModel: MovieItemViewModel
|
||||
@Default(.itemViewType)
|
||||
@Default(.Customization.itemViewType)
|
||||
private var itemViewType
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -29,10 +29,7 @@ extension SeriesItemView {
|
|||
// MARK: Genres
|
||||
|
||||
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
||||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
PillHStack(title: L10n.genres, items: genres).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -45,7 +42,7 @@ extension SeriesItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -55,12 +52,10 @@ extension SeriesItemView {
|
|||
// MARK: Cast and Crew
|
||||
|
||||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -68,12 +63,10 @@ extension SeriesItemView {
|
|||
// MARK: Similar
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ struct SeriesItemView: View {
|
|||
|
||||
@ObservedObject
|
||||
var viewModel: SeriesItemViewModel
|
||||
@Default(.itemViewType)
|
||||
@Default(.Customization.itemViewType)
|
||||
private var itemViewType
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -26,7 +26,7 @@ extension iPadOSCollectionItemView {
|
|||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ extension iPadOSCollectionItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -49,13 +49,10 @@ extension iPadOSCollectionItemView {
|
|||
// MARK: Items
|
||||
|
||||
if !viewModel.collectionItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.items,
|
||||
items: viewModel.collectionItems,
|
||||
itemWidth: 130
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.items, type: .portrait, items: viewModel.collectionItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
|
||||
ItemView.AboutView(viewModel: viewModel)
|
||||
|
|
|
@ -26,11 +26,10 @@ extension iPadOSEpisodeItemView {
|
|||
if let genres = viewModel.item.genreItems, !genres.isEmpty {
|
||||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres,
|
||||
selectedAction: { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
)
|
||||
items: genres
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -39,7 +38,7 @@ extension iPadOSEpisodeItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -49,13 +48,10 @@ extension iPadOSEpisodeItemView {
|
|||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
|
||||
!castAndCrew.isEmpty
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew,
|
||||
itemWidth: 130
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ extension iPadOSMovieItemView {
|
|||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
)
|
||||
.onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,7 @@ extension iPadOSMovieItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -52,13 +53,10 @@ extension iPadOSMovieItemView {
|
|||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
|
||||
!castAndCrew.isEmpty
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew,
|
||||
itemWidth: UIDevice.isIPad ? 130 : 110
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -66,13 +64,10 @@ extension iPadOSMovieItemView {
|
|||
// MARK: Similar
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems,
|
||||
itemWidth: UIDevice.isIPad ? 130 : 110
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ extension iPadOSSeriesItemView {
|
|||
PillHStack(
|
||||
title: L10n.genres,
|
||||
items: genres
|
||||
) { genre in
|
||||
).onSelect { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ extension iPadOSSeriesItemView {
|
|||
PillHStack(
|
||||
title: L10n.studios,
|
||||
items: studios
|
||||
) { studio in
|
||||
).onSelect { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
|
||||
|
@ -56,13 +56,10 @@ extension iPadOSSeriesItemView {
|
|||
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
|
||||
!castAndCrew.isEmpty
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
items: castAndCrew,
|
||||
itemWidth: 130
|
||||
) { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew)
|
||||
.onSelect { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
@ -70,13 +67,10 @@ extension iPadOSSeriesItemView {
|
|||
// MARK: Similar
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitPosterHStack(
|
||||
title: L10n.recommended,
|
||||
items: viewModel.similarItems,
|
||||
itemWidth: 130
|
||||
) { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems)
|
||||
.onSelect { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
|
|
@ -84,9 +84,10 @@ struct LibrarySearchView: View {
|
|||
if !items.isEmpty {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(items, id: \.id) { item in
|
||||
PortraitPosterButton(item: item) { item in
|
||||
searchRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterButton(item: item, type: .portrait)
|
||||
.onSelect { item in
|
||||
searchRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,9 +47,10 @@ struct LibraryView: View {
|
|||
VStack {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
PortraitPosterButton(item: item) { item in
|
||||
libraryRouter.route(to: \.item, item)
|
||||
}
|
||||
PosterButton(item: item, type: .portrait)
|
||||
.onSelect { item in
|
||||
libraryRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
|
|
|
@ -21,99 +21,86 @@ struct LiveTVProgramsView: View {
|
|||
if !viewModel.recommendedItems.isEmpty,
|
||||
let items = viewModel.recommendedItems
|
||||
{
|
||||
|
||||
PortraitPosterHStack(
|
||||
title: "On Now",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "On Now", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.seriesItems.isEmpty,
|
||||
let items = viewModel.seriesItems
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: "Shows",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "Shows", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.movieItems.isEmpty,
|
||||
let items = viewModel.movieItems
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: "Movies",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "Movies", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.sportsItems.isEmpty,
|
||||
let items = viewModel.sportsItems
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: "Sports",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "Sports", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.kidsItems.isEmpty,
|
||||
let items = viewModel.kidsItems
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: "Kids",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "Kids", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.newsItems.isEmpty,
|
||||
let items = viewModel.newsItems
|
||||
{
|
||||
PortraitPosterHStack(
|
||||
title: "News",
|
||||
items: items
|
||||
) { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
PosterHStack(title: "News", type: .portrait, items: items)
|
||||
.onSelect { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,24 +11,95 @@ import SwiftUI
|
|||
|
||||
struct CustomizeViewsSettings: View {
|
||||
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
@Default(.showFlattenView)
|
||||
@Default(.Customization.showFlattenView)
|
||||
var showFlattenView
|
||||
@Default(.Customization.itemViewType)
|
||||
var itemViewType
|
||||
|
||||
@Default(.shouldShowMissingSeasons)
|
||||
var shouldShowMissingSeasons
|
||||
@Default(.shouldShowMissingEpisodes)
|
||||
var shouldShowMissingEpisodes
|
||||
|
||||
@Default(.Customization.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.Customization.nextUpPosterType)
|
||||
var nextUpPosterType
|
||||
@Default(.Customization.recentlyAddedPosterType)
|
||||
var recentlyAddedPosterType
|
||||
@Default(.Customization.latestInLibraryPosterType)
|
||||
var latestInLibraryPosterType
|
||||
@Default(.Customization.recommendedPosterType)
|
||||
var recommendedPosterType
|
||||
|
||||
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
|
||||
var useSeriesLandscapeBackdrop
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
List {
|
||||
Section {
|
||||
|
||||
Toggle(L10n.showFlattenView, isOn: $showFlattenView)
|
||||
|
||||
Picker(L10n.items, selection: $itemViewType) {
|
||||
ForEach(ItemViewType.allCases, id: \.self) { type in
|
||||
Text(type.localizedName).tag(type.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
} header: {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(L10n.showMissingSeasons, isOn: $shouldShowMissingSeasons)
|
||||
Toggle(L10n.showMissingEpisodes, isOn: $shouldShowMissingEpisodes)
|
||||
} header: {
|
||||
L10n.missingItems.text
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||
Toggle(L10n.showFlattenView, isOn: $showFlattenView)
|
||||
|
||||
Picker(L10n.nextUp, selection: $nextUpPosterType) {
|
||||
ForEach(PosterType.allCases, id: \.self) { type in
|
||||
Text(type.localizedName).tag(type.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(L10n.recentlyAdded, selection: $recentlyAddedPosterType) {
|
||||
ForEach(PosterType.allCases, id: \.self) { type in
|
||||
Text(type.localizedName).tag(type.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Picker(L10n.library, selection: $latestInLibraryPosterType) {
|
||||
ForEach(PosterType.allCases, id: \.self) { type in
|
||||
Text(type.localizedName).tag(type.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Take time to do this for a lot of views
|
||||
// Picker(L10n.recommended, selection: $recommendedPosterType) {
|
||||
// ForEach(PosterType.allCases, id: \.self) { type in
|
||||
// Text(type.localizedName).tag(type.rawValue)
|
||||
// }
|
||||
// }
|
||||
|
||||
} header: {
|
||||
L10n.customize.text
|
||||
// TODO: localize after organization
|
||||
Text("Posters")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Series Backdrop", isOn: $useSeriesLandscapeBackdrop)
|
||||
} header: {
|
||||
// TODO: think of a better name
|
||||
// TODO: localize after organization
|
||||
Text("Episode Landscape Poster")
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.customize)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +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 Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct MissingItemsSettingsView: View {
|
||||
|
||||
@Default(.shouldShowMissingSeasons)
|
||||
var shouldShowMissingSeasons
|
||||
|
||||
@Default(.shouldShowMissingEpisodes)
|
||||
var shouldShowMissingEpisodes
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(L10n.showMissingSeasons, isOn: $shouldShowMissingSeasons)
|
||||
Toggle(L10n.showMissingEpisodes, isOn: $shouldShowMissingEpisodes)
|
||||
} header: {
|
||||
L10n.missingItems.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,8 +38,6 @@ struct SettingsView: View {
|
|||
var resumeOffset
|
||||
@Default(.subtitleSize)
|
||||
var subtitleSize
|
||||
@Default(.itemViewType)
|
||||
var itemViewType
|
||||
@Default(.subtitleFontName)
|
||||
var subtitleFontName
|
||||
|
||||
|
@ -148,24 +146,6 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Not localized yet. Will be in a settings re-organization
|
||||
Picker("Item View", selection: $itemViewType) {
|
||||
ForEach(ItemViewType.allCases, id: \.self) { itemViewType in
|
||||
Text(itemViewType.label).tag(itemViewType.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.missingSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.missingItems.text
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
|
|
Loading…
Reference in New Issue