Add ItemCoordinator
This commit is contained in:
parent
0640e7051d
commit
74a9302021
|
@ -143,7 +143,9 @@
|
|||
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; };
|
||||
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; };
|
||||
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; };
|
||||
6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; };
|
||||
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
|
||||
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
|
||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; };
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
||||
|
@ -366,6 +368,8 @@
|
|||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = "<group>"; };
|
||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = "<group>"; };
|
||||
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = "<group>"; };
|
||||
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = "<group>"; };
|
||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = "<group>"; };
|
||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
||||
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
|
||||
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||
|
@ -515,6 +519,7 @@
|
|||
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */,
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
|
@ -782,6 +787,7 @@
|
|||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||
);
|
||||
path = Coordinators;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1153,7 +1159,6 @@
|
|||
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
5398514526B64DA100101B49 /* SettingsView.swift in Sources */,
|
||||
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
|
||||
5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */,
|
||||
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
|
||||
62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */,
|
||||
|
@ -1193,6 +1198,7 @@
|
|||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||
53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */,
|
||||
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */,
|
||||
|
@ -1206,6 +1212,7 @@
|
|||
files = (
|
||||
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
|
@ -1213,6 +1220,7 @@
|
|||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||
62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
|
||||
|
|
|
@ -1,89 +1,86 @@
|
|||
//
|
||||
/*
|
||||
* 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 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
/*
|
||||
* 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 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct PortraitItemView: View {
|
||||
var item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: LazyView { ItemView(item: item) }) {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if item.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.6)
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(Color(.systemRed))
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
.padding(.leading, 2)
|
||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if item.userData?.played ?? false {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color(.white))
|
||||
.clipShape(Circle().scale(0.8))
|
||||
} else {
|
||||
if item.userData?.unplayedItemCount != nil {
|
||||
Capsule()
|
||||
.fill(Color.accentColor)
|
||||
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
|
||||
Text(String(item.userData!.unplayedItemCount ?? 0))
|
||||
.foregroundColor(.white)
|
||||
.font(.caption2)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}.padding(2)
|
||||
.fixedSize()
|
||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||
Text(item.seriesName ?? item.name ?? "")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if item.type == "Movie" || item.type == "Series" {
|
||||
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else if item.type == "Season" {
|
||||
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100),
|
||||
bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.overlay(Rectangle()
|
||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if item.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.6)
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(Color(.systemRed))
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
}.frame(width: 100)
|
||||
}
|
||||
.padding(.leading, 2)
|
||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if item.userData?.played ?? false {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color(.white))
|
||||
.clipShape(Circle().scale(0.8))
|
||||
} else {
|
||||
if item.userData?.unplayedItemCount != nil {
|
||||
Capsule()
|
||||
.fill(Color.accentColor)
|
||||
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
|
||||
Text(String(item.userData!.unplayedItemCount ?? 0))
|
||||
.foregroundColor(.white)
|
||||
.font(.caption2)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}.padding(2)
|
||||
.fixedSize()
|
||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||
Text(item.seriesName ?? item.name ?? "")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if item.type == "Movie" || item.type == "Series" {
|
||||
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else if item.type == "Season" {
|
||||
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}.frame(width: 100)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
|
||||
struct ProgressBar: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
@ -31,13 +32,17 @@ struct ProgressBar: Shape {
|
|||
}
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
|
||||
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: LazyView { ItemView(item: item) }) {
|
||||
Button {
|
||||
home.route(to: .item(viewModel: .init(id: item.id!)))
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||
.frame(width: 320, height: 180)
|
||||
|
|
|
@ -16,12 +16,18 @@ final class HomeCoordinator: NavigationCoordinatable {
|
|||
|
||||
enum Route: NavigationRoute {
|
||||
case settings
|
||||
case library(viewModel: LibraryViewModel, title: String)
|
||||
case item(viewModel: ItemViewModel)
|
||||
}
|
||||
|
||||
func resolveRoute(route: Route) -> Transition {
|
||||
switch route {
|
||||
case .settings:
|
||||
return .modal(SettingsCoordinator().eraseToAnyCoordinatable())
|
||||
case let .library(viewModel, title):
|
||||
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
|
||||
case let .item(viewModel):
|
||||
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
/*
|
||||
* 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 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class ItemCoordinator: NavigationCoordinatable {
|
||||
var navigationStack = NavigationStack()
|
||||
var viewModel: ItemViewModel
|
||||
|
||||
init(viewModel: ItemViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
enum Route: NavigationRoute {
|
||||
case item(viewModel: ItemViewModel)
|
||||
case library(viewModel: LibraryViewModel, title: String)
|
||||
}
|
||||
|
||||
func resolveRoute(route: Route) -> Transition {
|
||||
switch route {
|
||||
case let .item(viewModel):
|
||||
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
|
||||
case let .library(viewModel, title):
|
||||
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func start() -> some View {
|
||||
ItemView(viewModel: self.viewModel)
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
|||
enum Route: NavigationRoute {
|
||||
case search(viewModel: LibrarySearchViewModel)
|
||||
case filter(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||
case item(viewModel: ItemViewModel)
|
||||
}
|
||||
|
||||
func resolveRoute(route: Route) -> Transition {
|
||||
|
@ -35,6 +36,8 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
|||
enabledFilterType: enabledFilterType,
|
||||
parentId: parentId)
|
||||
.eraseToAnyCoordinatable())
|
||||
case let .item(viewModel):
|
||||
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,16 @@ final class SearchCoordinator: NavigationCoordinatable {
|
|||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
enum Route: NavigationRoute {}
|
||||
enum Route: NavigationRoute {
|
||||
case item(viewModel: ItemViewModel)
|
||||
}
|
||||
|
||||
func resolveRoute(route: Route) -> Transition {}
|
||||
func resolveRoute(route: Route) -> Transition {
|
||||
switch route {
|
||||
case let .item(viewModel):
|
||||
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func start() -> some View {
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct EpisodeItemView: View {
|
||||
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
|
||||
@StateObject var viewModel: EpisodeItemViewModel
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||
|
@ -15,7 +17,9 @@ struct EpisodeItemView: View {
|
|||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
||||
|
||||
var portraitHeaderView: some View {
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getBackdropImageBlurHash())
|
||||
ImageView(src: viewModel.item
|
||||
.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
.opacity(0.4)
|
||||
.blur(radius: 2.0)
|
||||
}
|
||||
|
@ -108,7 +112,10 @@ struct EpisodeItemView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView,
|
||||
overlayAlignment: .bottomLeading,
|
||||
headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds
|
||||
.width * 0.5625) {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
|
||||
|
@ -126,9 +133,9 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
|
||||
} label: {
|
||||
Text(genre.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -143,11 +150,13 @@ struct EpisodeItemView: View {
|
|||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.item.people!, id: \.self) { person in
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100),
|
||||
bh: person.getBlurHash())
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
|
@ -171,16 +180,16 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
}
|
||||
if !(viewModel.similarItems).isEmpty {
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
.font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
@ -189,7 +198,9 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.similarItems, id: \.self) { similarItem in
|
||||
NavigationLink(destination: LazyView { ItemView(item: similarItem) }) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: similarItem)
|
||||
}
|
||||
Spacer().frame(width: 10)
|
||||
|
@ -213,7 +224,8 @@ struct EpisodeItemView: View {
|
|||
.blur(radius: 4)
|
||||
HStack {
|
||||
VStack {
|
||||
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash())
|
||||
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120),
|
||||
bh: viewModel.item.getSeriesPrimaryImageBlurHash())
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
Spacer().frame(height: 15)
|
||||
|
@ -274,7 +286,7 @@ struct EpisodeItemView: View {
|
|||
Spacer()
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(x: 14)
|
||||
.padding(.top, 1)
|
||||
.padding(.top, 1)
|
||||
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Spacer()
|
||||
|
@ -318,9 +330,9 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
|
||||
} label: {
|
||||
Text(genre.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -337,14 +349,20 @@ struct EpisodeItemView: View {
|
|||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.item.people!, id: \.self) { person in
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item
|
||||
.route(to: .library(viewModel: .init(person: person),
|
||||
title: person.name ?? ""))
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!,
|
||||
maxWidth: 100),
|
||||
bh: person.getBlurHash())
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular)
|
||||
.lineLimit(1)
|
||||
.frame(width: 100).foregroundColor(Color.primary)
|
||||
if person.role != "" {
|
||||
Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||
|
@ -365,9 +383,9 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -376,7 +394,7 @@ struct EpisodeItemView: View {
|
|||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
}
|
||||
if !(viewModel.similarItems).isEmpty {
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
.font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
@ -385,7 +403,9 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.similarItems, id: \.self) { similarItem in
|
||||
NavigationLink(destination: LazyView { ItemView(item: similarItem) }) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: similarItem)
|
||||
}
|
||||
Spacer().frame(width: 10)
|
||||
|
|
|
@ -36,9 +36,9 @@ struct HomeView: View {
|
|||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
home.route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
|
|
|
@ -5,30 +5,30 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
class VideoPlayerItem: ObservableObject {
|
||||
@Published var shouldShowPlayer: Bool = false
|
||||
@Published var itemToPlay: BaseItemDto = BaseItemDto()
|
||||
@Published var itemToPlay = BaseItemDto()
|
||||
}
|
||||
|
||||
struct ItemView: View {
|
||||
private var item: BaseItemDto
|
||||
|
||||
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
||||
@State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view.
|
||||
@StateObject var viewModel: ItemViewModel
|
||||
@StateObject private var videoPlayerItem = VideoPlayerItem()
|
||||
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
|
||||
@State private var isLoading: Bool = false
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
||||
ZStack {
|
||||
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
||||
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
|
||||
loadBinding: $videoIsLoading,
|
||||
pBinding: _videoPlayerItem
|
||||
.projectedValue
|
||||
.shouldShowPlayer)
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.statusBar(hidden: true)
|
||||
|
@ -37,25 +37,30 @@ struct ItemView: View {
|
|||
}, isActive: $videoPlayerItem.shouldShowPlayer) {
|
||||
EmptyView()
|
||||
}
|
||||
VStack {
|
||||
if item.type == "Movie" {
|
||||
MovieItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Season" {
|
||||
SeasonItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Series" {
|
||||
SeriesItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Episode" {
|
||||
EpisodeItemView(viewModel: .init(item: item))
|
||||
} else {
|
||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||
Group {
|
||||
if let item = viewModel.item {
|
||||
if item.type == "Movie" {
|
||||
MovieItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Season" {
|
||||
SeasonItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Series" {
|
||||
SeriesItemView(viewModel: .init(item: item))
|
||||
} else if item.type == "Episode" {
|
||||
EpisodeItemView(viewModel: .init(item: item))
|
||||
} else {
|
||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||
}
|
||||
}
|
||||
}
|
||||
.introspectTabBarController { (UITabBarController) in
|
||||
.introspectTabBarController { UITabBarController in
|
||||
UITabBarController.tabBar.isHidden = false
|
||||
}
|
||||
.navigationBarHidden(false)
|
||||
.navigationBarBackButtonHidden(false)
|
||||
.environmentObject(videoPlayerItem)
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LatestMediaView: View {
|
||||
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
|
||||
@StateObject var viewModel: LatestMediaViewModel
|
||||
|
||||
var body: some View {
|
||||
|
@ -15,7 +17,11 @@ struct LatestMediaView: View {
|
|||
LazyHStack {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type == "Series" || item.type == "Movie" {
|
||||
PortraitItemView(item: item)
|
||||
Button {
|
||||
home.route(to: .item(viewModel: .init(id: item.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}.padding(.leading, 20)
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibrarySearchView: View {
|
||||
@EnvironmentObject var search: NavigationRouter<SearchCoordinator.Route>
|
||||
|
@ -80,7 +80,11 @@ struct LibrarySearchView: View {
|
|||
if !items.isEmpty {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(items, id: \.id) { item in
|
||||
PortraitItemView(item: item)
|
||||
Button {
|
||||
search.route(to: .item(viewModel: .init(id: item.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
|
@ -108,7 +112,6 @@ struct LibrarySearchView: View {
|
|||
}
|
||||
|
||||
private extension ItemType {
|
||||
|
||||
var localized: String {
|
||||
switch self {
|
||||
case .episode:
|
||||
|
|
|
@ -35,7 +35,11 @@ struct LibraryView: View {
|
|||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type != "Folder" {
|
||||
PortraitItemView(item: item)
|
||||
Button {
|
||||
library.route(to: .item(viewModel: .init(id: item.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onRotate { _ in
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct MovieItemView: View {
|
||||
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
|
||||
@StateObject var viewModel: MovieItemViewModel
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass)
|
||||
|
@ -18,7 +20,8 @@ struct MovieItemView: View {
|
|||
private var playbackInfo: VideoPlayerItem
|
||||
|
||||
var portraitHeaderView: some View {
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
|
||||
ImageView(src: viewModel.item
|
||||
.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
.opacity(0.4)
|
||||
.blur(radius: 2.0)
|
||||
|
@ -135,9 +138,9 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
|
||||
} label: {
|
||||
Text(genre.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -152,9 +155,9 @@ struct MovieItemView: View {
|
|||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.item.people!, id: \.self) { person in
|
||||
if person.type ?? "" == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100),
|
||||
|
@ -182,17 +185,16 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
}
|
||||
if !(viewModel.similarItems).isEmpty {
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
.font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
@ -201,7 +203,9 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.similarItems, id: \.self) { similarItem in
|
||||
NavigationLink(destination: LazyView { ItemView(item: similarItem) }) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: similarItem)
|
||||
}
|
||||
Spacer().frame(width: 10)
|
||||
|
@ -236,7 +240,8 @@ struct MovieItemView: View {
|
|||
self.playbackInfo.shouldShowPlayer = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
|
||||
Text(viewModel.item
|
||||
.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
|
||||
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
}
|
||||
|
@ -290,7 +295,7 @@ struct MovieItemView: View {
|
|||
Spacer()
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(x: 14)
|
||||
.padding(.top, 1)
|
||||
.padding(.top, 1)
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Spacer()
|
||||
HStack {
|
||||
|
@ -333,9 +338,9 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
|
||||
} label: {
|
||||
Text(genre.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -352,9 +357,11 @@ struct MovieItemView: View {
|
|||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.item.people!, id: \.self) { person in
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item
|
||||
.route(to: .library(viewModel: .init(person: person),
|
||||
title: person.name ?? ""))
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!,
|
||||
|
@ -384,9 +391,9 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +402,7 @@ struct MovieItemView: View {
|
|||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
}
|
||||
if !(viewModel.similarItems).isEmpty {
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
.font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
@ -404,7 +411,9 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(viewModel.similarItems, id: \.self) { similarItem in
|
||||
NavigationLink(destination: LazyView { ItemView(item: similarItem) }) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: similarItem)
|
||||
}
|
||||
Spacer().frame(width: 10)
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct NextUpView: View {
|
||||
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
|
||||
|
||||
var items: [BaseItemDto]
|
||||
|
||||
|
@ -22,7 +24,11 @@ struct NextUpView: View {
|
|||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
PortraitItemView(item: item)
|
||||
Button {
|
||||
home.route(to: .item(viewModel: .init(id: item.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct SeasonItemView: View {
|
||||
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
|
||||
@StateObject var viewModel: SeasonItemViewModel
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||
|
@ -18,7 +20,9 @@ struct SeasonItemView: View {
|
|||
if viewModel.isLoading {
|
||||
EmptyView()
|
||||
} else {
|
||||
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getSeriesBackdropImageBlurHash())
|
||||
ImageView(src: viewModel.item
|
||||
.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
|
||||
bh: viewModel.item.getSeriesBackdropImageBlurHash())
|
||||
.opacity(0.4)
|
||||
.blur(radius: 2.0)
|
||||
}
|
||||
|
@ -43,7 +47,7 @@ struct SeasonItemView: View {
|
|||
}
|
||||
}.offset(y: -32)
|
||||
}.padding(.horizontal, 16)
|
||||
.offset(y: 22)
|
||||
.offset(y: 22)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -63,42 +67,40 @@ struct SeasonItemView: View {
|
|||
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
|
||||
.padding(.trailing, 16)
|
||||
ForEach(viewModel.episodes, id: \.id) { episode in
|
||||
NavigationLink(destination: ItemView(item: episode)) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: episode.id!)))
|
||||
} label: {
|
||||
HStack {
|
||||
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
|
||||
.shadow(radius: 5)
|
||||
.frame(width: 150, height: 90)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if episode.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.6)
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(Color(.systemRed))
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
.overlay(Rectangle()
|
||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if episode.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.6)
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(Color(.systemRed))
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
.padding(.leading, 2)
|
||||
.padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if episode.userData?.played ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
}
|
||||
}.padding(2)
|
||||
}
|
||||
.padding(.leading, 2)
|
||||
.padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if episode.userData?.played ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
}
|
||||
}.padding(2)
|
||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
|
@ -131,9 +133,9 @@ struct SeasonItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +150,8 @@ struct SeasonItemView: View {
|
|||
} else {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), bh: viewModel.item.getSeriesBackdropImageBlurHash())
|
||||
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200),
|
||||
bh: viewModel.item.getSeriesBackdropImageBlurHash())
|
||||
.opacity(0.4)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
||||
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||
|
@ -180,22 +183,23 @@ struct SeasonItemView: View {
|
|||
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
|
||||
.padding(.trailing, 16)
|
||||
ForEach(viewModel.episodes, id: \.id) { episode in
|
||||
NavigationLink(destination: ItemView(item: episode)) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: episode.id!)))
|
||||
} label: {
|
||||
HStack {
|
||||
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
|
||||
.shadow(radius: 5)
|
||||
.frame(width: 150, height: 90)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.overlay(Rectangle()
|
||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline)
|
||||
Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
|
@ -224,9 +228,9 @@ struct SeasonItemView: View {
|
|||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(viewModel.item.studios!, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesItemView: View {
|
||||
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
|
||||
@StateObject var viewModel: SeriesItemViewModel
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||
|
@ -69,27 +71,46 @@ struct SeriesItemView: View {
|
|||
.padding(.horizontal, 16)
|
||||
}
|
||||
if let genreItems = viewModel.item.genreItems,
|
||||
!genreItems.isEmpty {
|
||||
!genreItems.isEmpty
|
||||
{
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 8) {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(genreItems, id: \.id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
|
||||
} label: {
|
||||
Text(genre.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
Text(viewModel.item.overview ?? "")
|
||||
.font(.footnote)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 16)
|
||||
if let studios = viewModel.item.studios,
|
||||
!studios.isEmpty
|
||||
{
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(studios, id: \.id) { studio in
|
||||
Button {
|
||||
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
} label: {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
Text("Seasons")
|
||||
.font(.callout).fontWeight(.semibold)
|
||||
.padding(.horizontal, 16)
|
||||
|
@ -97,14 +118,19 @@ struct SeriesItemView: View {
|
|||
.padding(.top, 24)
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.seasons, id: \.id) { season in
|
||||
PortraitItemView(item: season)
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: season.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: season)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 8)
|
||||
LazyVStack(alignment: .leading, spacing: 0) {
|
||||
if let people = viewModel.item.people,
|
||||
!people.isEmpty {
|
||||
!people.isEmpty
|
||||
{
|
||||
Text("CAST")
|
||||
.font(.callout).fontWeight(.semibold)
|
||||
.padding(.bottom, 8)
|
||||
|
@ -113,9 +139,11 @@ struct SeriesItemView: View {
|
|||
LazyHStack(spacing: 16) {
|
||||
ForEach(people, id: \.self) { person in
|
||||
if person.type == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
item
|
||||
.route(to: .library(viewModel: .init(person: person),
|
||||
title: person.name ?? ""))
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100),
|
||||
|
@ -125,7 +153,8 @@ struct SeriesItemView: View {
|
|||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
.frame(width: 100).foregroundColor(Color.primary)
|
||||
if let role = person.role,
|
||||
!role.isEmpty {
|
||||
!role.isEmpty
|
||||
{
|
||||
Text(role).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||
.foregroundColor(Color.secondary).frame(width: 100)
|
||||
}
|
||||
|
@ -138,23 +167,6 @@ struct SeriesItemView: View {
|
|||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
if let studios = viewModel.item.studios,
|
||||
!studios.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(studios, id: \.id) { studio in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
||||
}) {
|
||||
Text(studio.name ?? "").font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
Text("More Like This")
|
||||
.font(.callout).fontWeight(.semibold)
|
||||
|
@ -163,7 +175,9 @@ struct SeriesItemView: View {
|
|||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(viewModel.similarItems, id: \.self) { similarItem in
|
||||
NavigationLink(destination: LazyView { ItemView(item: similarItem) }) {
|
||||
Button {
|
||||
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
|
||||
} label: {
|
||||
PortraitItemView(item: similarItem)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
/*
|
||||
* 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 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
class ItemViewModel: ViewModel {
|
||||
var id: String
|
||||
|
||||
@Published var item: BaseItemDto?
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
super.init()
|
||||
|
||||
getRelatedItems()
|
||||
}
|
||||
|
||||
func getRelatedItems() {
|
||||
UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
}, receiveValue: { [weak self] response in
|
||||
self?.item = response
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue