Some work (#552)

This commit is contained in:
Ethan Pippin 2022-09-03 10:20:12 -06:00 committed by GitHub
parent 1fe2f1d30c
commit 79476328fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 308 additions and 232 deletions

View File

@ -38,7 +38,8 @@ final class MainCoordinator: NavigationCoordinatable {
UIScrollView.appearance().keyboardDismissMode = .onDrag
// Back bar button item setup
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
let config = UIImage.SymbolConfiguration(paletteColors: [.white, .jellyfinPurple])
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill", withConfiguration: config)
let barAppearance = UINavigationBar.appearance()
barAppearance.backIndicatorImage = backButtonBackgroundImage
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage

View File

@ -42,19 +42,19 @@ extension BaseItemDto {
return text
}
func getItemProgressString() -> String? {
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
return nil
}
var progress: String? {
guard let playbackPositionTicks = userData?.playbackPositionTicks,
let totalTicks = runTimeTicks,
playbackPositionTicks != 0,
totalTicks != 0 else { return nil }
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if proghours != 0 {
return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
} else {
return "\(String(progminutes))m"
}
let remainingSeconds = (totalTicks - playbackPositionTicks) / 10_000_000
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .abbreviated
return formatter.string(from: .init(remainingSeconds))
}
func getLiveStartTimeString(formatter: DateFormatter) -> String {

View File

@ -46,6 +46,9 @@ extension View {
}
}
// TODO: Simplify plethora of calls
// TODO: Centralize math
// TODO: Move poster stuff to own file
func posterStyle(type: PosterType, width: CGFloat) -> some View {
Group {
switch type {
@ -57,17 +60,35 @@ extension View {
}
}
/// Applies Portrait Poster frame with proper corner radius ratio against the width
func portraitPoster(width: CGFloat) -> some View {
func posterStyle(type: PosterType, height: CGFloat) -> some View {
Group {
switch type {
case .portrait:
self.portraitPoster(height: height)
case .landscape:
self.landscapePoster(height: height)
}
}
}
private 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 {
private func landscapePoster(width: CGFloat) -> some View {
self.frame(width: width, height: width / 1.77)
.cornerRadius(width / 30)
}
private func portraitPoster(height: CGFloat) -> some View {
self.portraitPoster(width: height / 1.5)
}
private func landscapePoster(height: CGFloat) -> some View {
self.landscapePoster(width: height * 1.77)
}
@inlinable
func padding2(_ edges: Edge.Set = .all) -> some View {
self.padding(edges)

View File

@ -6,6 +6,7 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Combine
import Defaults
import JellyfinAPI
import SwiftUI
@ -63,9 +64,10 @@ extension EpisodesRowManager {
TvShowsAPI.getEpisodes(
seriesId: item.id ?? "",
userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
fields: [.overview, .seasonUserData],
seasonId: seasonID,
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false
isMissing: Defaults[.shouldShowMissingEpisodes] ? nil : false,
enableUserData: true
)
.trackActivity(loading)
.sink { completion in

View File

@ -13,20 +13,13 @@ import Stinsen
final class EpisodeItemViewModel: ItemViewModel {
@RouterObject
private var itemRouter: ItemCoordinator.Router?
@Published
var playButtonText: String = ""
@Published
var mediaDetailItems: [[BaseItemDto.ItemDetail]] = []
var seriesItem: BaseItemDto?
override init(item: BaseItemDto) {
super.init(item: item)
$videoPlayerViewModels.sink(receiveValue: { newValue in
self.mediaDetailItems = self.createMediaDetailItems(viewModels: newValue)
})
.store(in: &cancellables)
getSeriesItem()
}
override func updateItem() {
@ -56,29 +49,23 @@ final class EpisodeItemViewModel: ItemViewModel {
.store(in: &cancellables)
}
private func createMediaDetailItems(viewModels: [VideoPlayerViewModel]) -> [[BaseItemDto.ItemDetail]] {
var fileMediaItems: [[BaseItemDto.ItemDetail]] = []
private func getSeriesItem() {
guard let seriesID = item.seriesId else { return }
for viewModel in viewModels {
let audioStreams = viewModel.audioStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
.joined(separator: ", ")
let subtitleStreams = viewModel.subtitleStreams
.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
.joined(separator: ", ")
let currentMediaItems: [BaseItemDto.ItemDetail] = [
.init(title: "File", content: viewModel.filename ?? .emptyDash),
.init(title: "Audio", content: audioStreams),
.init(title: "Subtitles", content: subtitleStreams),
]
fileMediaItems.append(currentMediaItems)
}
// print(fileMediaItems)
return fileMediaItems
ItemsAPI.getItems(
userId: SessionManager.main.currentLogin.user.id,
limit: 1,
fields: ItemFields.allCases,
enableUserData: true,
ids: [seriesID]
)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
guard let firstItem = response.items?.first else { return }
self?.seriesItem = firstItem
})
.store(in: &cancellables)
}
}

View File

@ -70,8 +70,8 @@ class ItemViewModel: ViewModel {
}
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
guard item.type == .episode || item.type == .movie else { return }
guard !item.missing, !item.unaired else { return }
guard item.type == .episode || item.type == .movie,
!item.missing else { return }
item.createVideoPlayerViewModel()
.sink { completion in
@ -93,7 +93,7 @@ class ItemViewModel: ViewModel {
return L10n.missing
}
if let itemProgressString = item.getItemProgressString() {
if let itemProgressString = item.progress {
return itemProgressString
}

View File

@ -0,0 +1,51 @@
//
// 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 LandscapePosterProgressBar: View {
let title: String
let progress: CGFloat
// Scale padding depending on view width
@State
private var paddingScale: CGFloat = 1.0
var body: some View {
GeometryReader { reader in
ZStack(alignment: .bottom) {
LinearGradient(
stops: [
.init(color: .clear, location: 0),
.init(color: .black.opacity(0.7), location: 1),
],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 40)
VStack(alignment: .leading, spacing: 3 * paddingScale) {
Spacer()
Text(title)
.font(.subheadline)
.foregroundColor(.white)
ProgressBar(progress: progress)
}
.padding(.horizontal, 5 * paddingScale)
.padding(.bottom, 7 * paddingScale)
.onAppear {
paddingScale = reader.size.width / 300
}
}
}
}
}

View File

@ -0,0 +1,28 @@
//
// 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 ProgressBar: View {
let progress: CGFloat
var body: some View {
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(.white)
.opacity(0.2)
Capsule()
.foregroundColor(.jellyfinPurple)
.scaleEffect(x: progress, y: 1, anchor: .leading)
}
.frame(maxWidth: .infinity)
.frame(height: 3)
}
}

View File

@ -47,7 +47,7 @@ struct CinematicResumeCardView: View {
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "")
Text(item.progress ?? "")
.font(.subheadline)
.padding(.vertical, 5)
.padding(.leading, 10)

View File

@ -81,7 +81,7 @@ struct LandscapeItemElement: View {
.frame(width: 445, height: 90)
.mask(CutOffShadow())
VStack(alignment: .leading) {
Text("CONTINUE • \(item.getItemProgressString() ?? "")")
Text("CONTINUE • \(item.progress ?? "")")
.font(.caption)
.fontWeight(.medium)
.offset(y: 5)

View File

@ -34,7 +34,7 @@ struct ContinueWatchingCard: View {
}
VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "")
Text(item.progress ?? "")
.font(.subheadline)
.padding(.vertical, 5)
.padding(.leading, 10)

View File

@ -41,7 +41,7 @@ extension ItemView {
.failure {
InitialFailureView(viewModel.item.title.initials)
}
.portraitPoster(width: 270)
.posterStyle(type: .portrait, width: 270)
AboutViewCard(
isShowingAlert: $presentOverviewAlert,

View File

@ -525,6 +525,10 @@
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD09526C47118007C8DCF /* ErrorMessage.swift */; };
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; };
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; };
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; };
E1FE69AB28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -949,6 +953,8 @@
E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemContentView.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; };
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -1446,6 +1452,7 @@
isa = PBXGroup;
children = (
E18E01A7288746AF0022598C /* DotHStack.swift */,
E1FE69AF28C2DA4A0021BC93 /* FilterDrawerHStack */,
E18E01A5288746AF0022598C /* PillHStack.swift */,
E16AA60728A364A6009A983C /* PosterButton.swift */,
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */,
@ -2055,7 +2062,9 @@
E18E01FF288749200022598C /* Divider.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */,
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */,
E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */,
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
E1FE69A628C29B720021BC93 /* ProgressBar.swift */,
E1E1643D28BB074000323B0A /* SelectorView.swift */,
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */,
);
@ -2074,8 +2083,6 @@
E1C55AB228BD051700A9AD88 /* Components */ = {
isa = PBXGroup;
children = (
E113133528BE98AA00930F75 /* FilterDrawerButton.swift */,
E113133328BE988200930F75 /* FilterDrawerHStack.swift */,
E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */,
);
path = Components;
@ -2190,6 +2197,15 @@
path = Errors;
sourceTree = "<group>";
};
E1FE69AF28C2DA4A0021BC93 /* FilterDrawerHStack */ = {
isa = PBXGroup;
children = (
E113133528BE98AA00930F75 /* FilterDrawerButton.swift */,
E113133328BE988200930F75 /* FilterDrawerHStack.swift */,
);
path = FilterDrawerHStack;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -2484,6 +2500,7 @@
E18E021A2887492B0022598C /* AppIcon.swift in Sources */,
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */,
E1FE69AB28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */,
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
@ -2623,6 +2640,7 @@
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */,
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */,
E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */,
E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */,
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
@ -2763,6 +2781,7 @@
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */,
E113133228BDC72000930F75 /* FilterView.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */,
@ -2803,6 +2822,7 @@
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */,
E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */,
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */,
E18E01E9288747230022598C /* SeriesItemView.swift in Sources */,
E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */,

View File

@ -53,10 +53,10 @@ extension FilterDrawerHStack {
.foregroundColor(activated ? .jellyfinPurple : Color(UIColor.secondarySystemFill))
.opacity(0.5)
}
.overlay(
.overlay {
Capsule()
.stroke(activated ? .purple : Color(UIColor.secondarySystemFill), lineWidth: 1)
)
}
}
}
}

View File

@ -10,15 +10,15 @@ import SwiftUI
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
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 item: Item
private var type: PosterType
private var itemScale: CGFloat
private var horizontalAlignment: HorizontalAlignment
private var content: (Item) -> Content
private var imageOverlay: (Item) -> ImageOverlay
private var contextMenu: (Item) -> ContextMenu
private var onSelect: (Item) -> Void
private var singleImage: Bool
private var itemWidth: CGFloat {
type.width * itemScale
@ -96,34 +96,16 @@ extension PosterButton where Content == PosterButtonDefaultContentView<Item>,
}
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
)
func horizontalAlignment(_ alignment: HorizontalAlignment) -> Self {
var copy = self
copy.horizontalAlignment = alignment
return copy
}
@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
)
func scaleItem(_ scale: CGFloat) -> Self {
var copy = self
copy.itemScale = scale
return copy
}
@ViewBuilder
@ -171,19 +153,10 @@ extension PosterButton {
)
}
@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
)
func onSelect(_ action: @escaping (Item) -> Void) -> Self {
var copy = self
copy.onSelect = action
return copy
}
}

View File

@ -6,19 +6,20 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import CollectionView
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 var title: String
private var type: PosterType
private var items: [Item]
private var itemScale: CGFloat
private var content: (Item) -> Content
private var imageOverlay: (Item) -> ImageOverlay
private var contextMenu: (Item) -> ContextMenu
private var trailingContent: () -> TrailingContent
private var onSelect: (Item) -> Void
private init(
title: String,
@ -103,19 +104,11 @@ extension PosterHStack where Content == PosterButtonDefaultContentView<Item>,
}
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
)
func scaleItems(_ scale: CGFloat) -> Self {
var copy = self
copy.itemScale = scale
return copy
}
@ViewBuilder
@ -182,18 +175,9 @@ extension PosterHStack {
)
}
@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
)
func onSelect(_ action: @escaping (Item) -> Void) -> Self {
var copy = self
copy.onSelect = action
return copy
}
}

View File

@ -30,35 +30,10 @@ struct ContinueWatchingView: View {
}
}
.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)
}
}
}
}
LandscapePosterProgressBar(
title: item.progress ?? L10n.continue,
progress: (item.userData?.playedPercentage ?? 0) / 100
)
}
}
}

View File

@ -6,6 +6,7 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import CollectionView
import Defaults
import SwiftUI

View File

@ -35,7 +35,7 @@ extension ItemView {
viewModel.item.type == .episode ? viewModel.item.seriesImageSource(.primary, maxWidth: 300) : viewModel
.item.imageSource(.primary, maxWidth: 300)
)
.portraitPoster(width: 130)
.posterStyle(type: .portrait, width: 130)
.accessibilityIgnoresInvertColors()
Button {

View File

@ -36,7 +36,7 @@ extension ItemView {
)
} else {
Image(systemName: "checkmark.circle")
.foregroundStyle(.white)
// .foregroundStyle(.white)
}
}
.buttonStyle(PlainButtonStyle())
@ -54,7 +54,7 @@ extension ItemView {
.foregroundStyle(Color.red)
} else {
Image(systemName: "heart")
.foregroundStyle(.white)
// .foregroundStyle(.white)
}
}
.buttonStyle(PlainButtonStyle())

View File

@ -9,20 +9,26 @@
import JellyfinAPI
import SwiftUI
struct EpisodeCard: View {
struct EpisodeCard<RowManager: EpisodesRowManager>: View {
@EnvironmentObject
private var itemRouter: ItemCoordinator.Router
private var router: ItemCoordinator.Router
@ScaledMetric
private var staticOverviewHeight: CGFloat = 50
let viewModel: RowManager
let episode: BaseItemDto
var body: some View {
PosterButton(item: episode, type: .landscape, singleImage: true)
.scaleItem(1.2)
.imageOverlay { _ in
if episode.userData?.played ?? false {
if let progress = episode.progress {
LandscapePosterProgressBar(
title: progress,
progress: (episode.userData?.playedPercentage ?? 0) / 100
)
} else if episode.userData?.played ?? false {
ZStack(alignment: .bottomTrailing) {
Color.clear
@ -35,34 +41,53 @@ struct EpisodeCard: View {
}
}
.content { _ in
VStack(alignment: .leading) {
Text(episode.episodeLocator ?? L10n.unknown)
.font(.footnote)
.foregroundColor(.secondary)
Button {
router.route(to: \.item, episode)
} label: {
VStack(alignment: .leading) {
Text(episode.episodeLocator ?? L10n.unknown)
.font(.footnote)
.foregroundColor(.secondary)
Text(episode.displayName)
.font(.body)
.padding(.bottom, 1)
.lineLimit(2)
Text(episode.displayName)
.font(.body)
.foregroundColor(.primary)
.padding(.bottom, 1)
.lineLimit(2)
.multilineTextAlignment(.leading)
ZStack(alignment: .topLeading) {
Color.clear
.frame(height: staticOverviewHeight)
ZStack(alignment: .topLeading) {
Color.clear
.frame(height: staticOverviewHeight)
if episode.unaired {
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
} else {
Text(episode.overview ?? L10n.noOverviewAvailable)
if episode.unaired {
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
} else {
Text(episode.overview ?? L10n.noOverviewAvailable)
}
}
.font(.caption.weight(.light))
.foregroundColor(.secondary)
.lineLimit(4)
.multilineTextAlignment(.leading)
L10n.seeMore.text
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(.jellyfinPurple)
}
.font(.caption.weight(.light))
.foregroundColor(.secondary)
.lineLimit(4)
.multilineTextAlignment(.leading)
}
}
.onSelect { _ in
itemRouter.route(to: \.item, episode)
episode.createVideoPlayerViewModel()
.sink { completion in
self.viewModel.handleAPIRequestError(completion: completion)
} receiveValue: { viewModels in
if let episodeViewModel = viewModels.first {
router.route(to: \.videoPlayer, episodeViewModel)
}
}
.store(in: &viewModel.cancellables)
}
}
}

View File

@ -59,16 +59,16 @@ struct SeriesEpisodesView<RowManager: EpisodesRowManager>: View {
HStack(alignment: .top, spacing: 15) {
if viewModel.isLoading {
ForEach(0 ..< 5) { _ in
EpisodeCard(episode: .placeHolder)
EpisodeCard(viewModel: viewModel, episode: .placeHolder)
.redacted(reason: .placeholder)
}
} else if let selectedSeason = viewModel.selectedSeason {
if let seasonEpisodes = viewModel.seasonsEpisodes[selectedSeason] {
if seasonEpisodes.isEmpty {
EpisodeCard(episode: .noResults)
EpisodeCard(viewModel: viewModel, episode: .noResults)
} else {
ForEach(seasonEpisodes) { episode in
EpisodeCard(episode: episode)
EpisodeCard(viewModel: viewModel, episode: episode)
.id(episode.id)
}
}

View File

@ -14,7 +14,7 @@ extension EpisodeItemView {
struct ContentView: View {
@EnvironmentObject
private var itemRouter: ItemCoordinator.Router
private var router: ItemCoordinator.Router
@ObservedObject
var viewModel: EpisodeItemViewModel
@ -35,7 +35,7 @@ extension EpisodeItemView {
if let itemOverview = viewModel.item.overview {
TruncatedTextView(text: itemOverview) {
itemRouter.route(to: \.itemOverview, viewModel.item)
router.route(to: \.itemOverview, viewModel.item)
}
.font(.footnote)
.lineLimit(5)
@ -68,6 +68,15 @@ extension EpisodeItemView {
Divider()
}
// MARK: Series
if let seriesItem = viewModel.seriesItem {
PosterHStack(title: L10n.series, type: .portrait, items: [seriesItem])
.onSelect { item in
router.route(to: \.item, item)
}
}
// MARK: Details
if let informationItems = viewModel.item.createInformationItems(), !informationItems.isEmpty {
@ -131,6 +140,7 @@ extension EpisodeItemView.ContentView {
ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title)
.frame(maxWidth: 300)
.foregroundStyle(.primary)
}
}
}

View File

@ -26,7 +26,7 @@ struct EpisodeItemView: View {
.navBarOffset(
$scrollViewOffset,
start: 0,
end: 10
end: 30
)
}
}

View File

@ -152,6 +152,7 @@ extension ItemView.CinematicScrollView {
ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title)
.frame(maxWidth: 300)
.foregroundColor(.white)
}
.frame(maxWidth: .infinity)

View File

@ -183,6 +183,7 @@ extension ItemView.CompactLogoScrollView {
ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title)
.frame(maxWidth: 300)
.foregroundColor(.white)
}
}
}

View File

@ -179,7 +179,7 @@ extension ItemView.CompactPosterScrollView {
// MARK: Portrait Image
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
.portraitPoster(width: 130)
.posterStyle(type: .portrait, width: 130)
.accessibilityIgnoresInvertColors()
rightShelfView
@ -197,6 +197,7 @@ extension ItemView.CompactPosterScrollView {
ItemView.ActionButtonHStack(viewModel: viewModel, equalSpacing: false)
.font(.title2)
.foregroundColor(.white)
}
}
}

View File

@ -14,7 +14,7 @@ extension iPadOSEpisodeItemView {
struct ContentView: View {
@EnvironmentObject
private var itemRouter: ItemCoordinator.Router
private var router: ItemCoordinator.Router
@ObservedObject
var viewModel: EpisodeItemViewModel
@ -47,6 +47,15 @@ extension iPadOSEpisodeItemView {
Divider()
}
// MARK: Series
if let seriesItem = viewModel.seriesItem {
PosterHStack(title: L10n.series, type: .portrait, items: [seriesItem])
.onSelect { item in
router.route(to: \.item, item)
}
}
ItemView.AboutView(viewModel: viewModel)
}
}

View File

@ -159,6 +159,7 @@ extension ItemView.iPadOSCinematicScrollView {
ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title)
.foregroundColor(.white)
}
.frame(width: 250)
}

View File

@ -21,9 +21,8 @@ struct LibraryItemRow: View {
router.route(to: \.item, item)
} label: {
HStack(alignment: .bottom) {
PosterButton(item: item, type: .portrait)
.scaleItem(0.6)
.content { _ in }
ImageView(item.portraitPosterImageSource(maxWidth: 60))
.posterStyle(type: .portrait, width: 60)
VStack(alignment: .leading) {
Text(item.displayName)

View File

@ -31,15 +31,12 @@ struct CustomizeViewsSettings: View {
var recommendedPosterType
@Default(.Customization.searchPosterType)
var searchPosterType
@Default(.Customization.Library.gridPosterType)
var libraryGridPosterType
@Default(.Customization.Episodes.useSeriesLandscapeBackdrop)
var useSeriesLandscapeBackdrop
@Default(.Customization.Library.gridPosterType)
var libraryGridPosterType
@Default(.Customization.Library.viewType)
var libraryViewType
var body: some View {
List {
Section {
@ -94,6 +91,12 @@ struct CustomizeViewsSettings: View {
Text(type.localizedName).tag(type.rawValue)
}
}
Picker(L10n.library, selection: $libraryGridPosterType) {
ForEach(PosterType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
} header: {
// TODO: localize after organization
Text("Posters")
@ -106,23 +109,6 @@ struct CustomizeViewsSettings: View {
// TODO: localize after organization
Text("Episode Landscape Poster")
}
Section {
Picker(L10n.library, selection: $libraryGridPosterType) {
ForEach(PosterType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
Picker(L10n.items, selection: $libraryViewType) {
ForEach(LibraryViewType.allCases, id: \.self) { type in
Text(type.localizedName).tag(type.rawValue)
}
}
} header: {
// TODO: localize after organization
Text("Library")
}
}
.navigationTitle(L10n.customize)
}