iOS advanced seasons episodes selection
This commit is contained in:
parent
1f199dc4f8
commit
78c061d4de
|
@ -12,55 +12,8 @@ import SwiftUI
|
|||
struct ItemDetailsView: View {
|
||||
|
||||
@ObservedObject var viewModel: ItemViewModel
|
||||
private let detailItems: [(String, String)]
|
||||
private let mediaItems: [(String, String)]
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
init(viewModel: ItemViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
var initialDetailItems: [(String, String)] = []
|
||||
|
||||
if let productionYear = viewModel.item.productionYear {
|
||||
initialDetailItems.append(("Released", "\(productionYear)"))
|
||||
}
|
||||
|
||||
if let rating = viewModel.item.officialRating {
|
||||
initialDetailItems.append(("Rated", "\(rating)"))
|
||||
}
|
||||
|
||||
if let runtime = viewModel.item.getItemRuntime() {
|
||||
initialDetailItems.append(("Runtime", "\(runtime)"))
|
||||
}
|
||||
|
||||
var initialMediatems: [(String, String)] = []
|
||||
|
||||
if let container = viewModel.item.container {
|
||||
let containerList = container.split(separator: ",")
|
||||
if containerList.count > 1 {
|
||||
initialMediatems.append(("Containers", containerList.joined(separator: ", ")))
|
||||
} else {
|
||||
initialMediatems.append(("Container", containerList.joined(separator: ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
|
||||
|
||||
if !itemVideoPlayerViewModel.audioStreams.isEmpty {
|
||||
let audioList = itemVideoPlayerViewModel.audioStreams.compactMap({ $0.displayTitle }).joined(separator: ", ")
|
||||
initialMediatems.append(("Audio", audioList))
|
||||
}
|
||||
|
||||
if !itemVideoPlayerViewModel.subtitleStreams.isEmpty {
|
||||
let subtitlesList = itemVideoPlayerViewModel.subtitleStreams.compactMap({ $0.displayTitle }).joined(separator: ", ")
|
||||
initialMediatems.append(("Subtitles", subtitlesList))
|
||||
}
|
||||
}
|
||||
|
||||
detailItems = initialDetailItems
|
||||
mediaItems = initialMediatems
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
|
|
|
@ -13,8 +13,9 @@ struct LatestMediaView: View {
|
|||
|
||||
@StateObject var tempViewModel = ViewModel()
|
||||
@State var items: [BaseItemDto] = []
|
||||
private var library_id: String = ""
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
private var library_id: String = ""
|
||||
|
||||
init(usingParentID: String) {
|
||||
library_id = usingParentID
|
||||
|
@ -26,15 +27,13 @@ struct LatestMediaView: View {
|
|||
}
|
||||
viewDidLoad = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], enableUserData: true, limit: 12)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
items = response
|
||||
})
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], enableUserData: true, limit: 12)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
items = response
|
||||
})
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; };
|
||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; };
|
||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
||||
53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
||||
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
||||
|
@ -230,6 +229,13 @@
|
|||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
||||
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */; };
|
||||
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */; };
|
||||
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||
E10D87E0278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
|
||||
E10D87E227852FD000BD264C /* EpisodesRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */; };
|
||||
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */; };
|
||||
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
||||
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
||||
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
||||
|
@ -493,7 +499,6 @@
|
|||
5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||
5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
|
||||
53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||
53913BCA26D323FE00EB3286 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
53913BCD26D323FE00EB3286 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -587,6 +592,10 @@
|
|||
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
|
||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
|
||||
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
|
||||
E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowViewModel.swift; sourceTree = "<group>"; };
|
||||
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||
|
@ -767,6 +776,7 @@
|
|||
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */,
|
||||
E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */,
|
||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||
62E632F2267D54030063E547 /* ItemViewModel.swift */,
|
||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
|
||||
|
@ -891,6 +901,7 @@
|
|||
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
||||
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
||||
E10D87DD278510E300BD264C /* PosterSize.swift */,
|
||||
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
||||
535870AC2669D8DD00D05A09 /* Typings.swift */,
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
||||
|
@ -1299,7 +1310,6 @@
|
|||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||
53892771263C8C6F0035E14B /* LoadingView.swift */,
|
||||
5389276F263C25230035E14B /* NextUpView.swift */,
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||
|
@ -1315,7 +1325,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
||||
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */,
|
||||
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */,
|
||||
E18845FB26DEACC400B0C5B7 /* Landscape */,
|
||||
E18845FA26DEACBE00B0C5B7 /* Portrait */,
|
||||
);
|
||||
|
@ -1955,6 +1967,7 @@
|
|||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
||||
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */,
|
||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
||||
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
|
||||
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
||||
|
@ -2020,6 +2033,7 @@
|
|||
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
|
||||
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
|
||||
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
|
||||
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
||||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
||||
|
@ -2066,6 +2080,7 @@
|
|||
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||
E10D87E227852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
|
@ -2100,6 +2115,7 @@
|
|||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
||||
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */,
|
||||
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
|
||||
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
|
||||
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||
|
@ -2125,6 +2141,7 @@
|
|||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */,
|
||||
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
||||
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
|
||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||
|
@ -2172,10 +2189,10 @@
|
|||
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
|
||||
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */,
|
||||
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -2195,6 +2212,7 @@
|
|||
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
|
||||
6264E88E273850380081A12A /* Strings.swift in Sources */,
|
||||
E10D87E0278510E400BD264C /* PosterSize.swift in Sources */,
|
||||
E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
||||
|
|
|
@ -60,7 +60,6 @@ struct EpisodeCardVStackView: View {
|
|||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color.jellyfinPurple)
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
|
|
|
@ -13,7 +13,6 @@ struct PillHStackView<ItemType: PillStackable>: View {
|
|||
|
||||
let title: String
|
||||
let items: [ItemType]
|
||||
// let navigationView: (ItemType) -> NavigationView
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -12,66 +12,70 @@ import SwiftUI
|
|||
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||
|
||||
let items: [ItemType]
|
||||
let maxWidth: Int
|
||||
let maxWidth: CGFloat
|
||||
let horizontalAlignment: HorizontalAlignment
|
||||
let textAlignment: TextAlignment
|
||||
let topBarView: () -> TopBarView
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) {
|
||||
init(items: [ItemType],
|
||||
maxWidth: CGFloat = 110,
|
||||
horizontalAlignment: HorizontalAlignment = .leading,
|
||||
textAlignment: TextAlignment = .leading,
|
||||
topBarView: @escaping () -> TopBarView,
|
||||
selectedAction: @escaping (ItemType) -> Void) {
|
||||
self.items = items
|
||||
self.maxWidth = maxWidth
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.textAlignment = textAlignment
|
||||
self.topBarView = topBarView
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
topBarView()
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
HStack(alignment: .top) {
|
||||
|
||||
Spacer().frame(width: 16)
|
||||
|
||||
ForEach(items, id: \.title) { item in
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
|
||||
bh: item.blurHash,
|
||||
failureInitials: item.failureInitials)
|
||||
.frame(width: 100, height: CGFloat(maxWidth))
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
HStack(alignment: .top, spacing: 15) {
|
||||
ForEach(items, id: \.self.portraitImageID) { item in
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
|
||||
bh: item.blurHash,
|
||||
failureInitials: item.failureInitials)
|
||||
.frame(width: maxWidth, height: maxWidth * 1.5)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
.frame(width: 100)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let description = item.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.frame(width: 100)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
}
|
||||
if let description = item.subtitle {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.frame(width: maxWidth)
|
||||
}
|
||||
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
}
|
||||
}.padding(.top, -3)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ struct PortraitItemView: View {
|
|||
.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 {
|
||||
|
|
|
@ -9,71 +9,79 @@
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ProgressBar: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
let tl = CGPoint(x: rect.minX, y: rect.minY)
|
||||
let tr = CGPoint(x: rect.maxX, y: rect.minY)
|
||||
let br = CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
let bls = CGPoint(x: rect.minX + 10, y: rect.maxY)
|
||||
let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10)
|
||||
|
||||
path.move(to: tl)
|
||||
path.addLine(to: tr)
|
||||
path.addLine(to: br)
|
||||
path.addLine(to: bls)
|
||||
path.addRelativeArc(center: blc, radius: 10,
|
||||
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
var items: [BaseItemDto]
|
||||
@ObservedObject var viewModel: HomeViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
ForEach(viewModel.resumeItems, id: \.id) { item in
|
||||
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||
.frame(width: 320, height: 180)
|
||||
.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) * 3.2), height: 7)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
HStack {
|
||||
|
||||
ZStack {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||
.frame(width: 320, height: 180)
|
||||
|
||||
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() ?? "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.getEpisodeLocator() ?? "") - \(item.name ?? "")")
|
||||
|
||||
if item.itemType == .episode {
|
||||
Text(item.getEpisodeLocator() ?? "")
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.offset(x: -1.4)
|
||||
}
|
||||
Spacer()
|
||||
}.frame(width: 320, alignment: .leading)
|
||||
}.padding(.top, 10)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}.frame(height: 215)
|
||||
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,35 +51,49 @@ struct HomeView: View {
|
|||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
if !viewModel.resumeItems.isEmpty {
|
||||
ContinueWatchingView(items: viewModel.resumeItems)
|
||||
ContinueWatchingView(viewModel: viewModel)
|
||||
}
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
}
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
HStack {
|
||||
Text(L10n.latestWithString(library.name ?? ""))
|
||||
PortraitImageHStackView(items: viewModel.nextUpItems,
|
||||
horizontalAlignment: .leading) {
|
||||
L10n.nextUp.text
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
Button {
|
||||
homeRouter
|
||||
.route(to: \.library, (viewModel: .init(parentID: library.id!,
|
||||
filters: viewModel.recentFilterSet),
|
||||
title: library.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.seeAll.text.font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
.padding()
|
||||
} selectedAction: { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ForEach(viewModel.libraries, id: \.self) { library in
|
||||
|
||||
LatestMediaView(viewModel: LatestMediaViewModel(libraryID: library.id!)) {
|
||||
HStack {
|
||||
Text(L10n.latestWithString(library.name ?? ""))
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
homeRouter
|
||||
.route(to: \.library, (viewModel: .init(parentID: library.id!,
|
||||
filters: viewModel.recentFilterSet),
|
||||
title: library.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.seeAll.text.font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16)
|
||||
.padding(.trailing, 16)
|
||||
LatestMediaView(viewModel: .init(libraryID: library.id!))
|
||||
.padding()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.introspectScrollView { scrollView in
|
||||
let control = UIRefreshControl()
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
//
|
||||
/*
|
||||
* 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
|
||||
|
||||
struct EpisodesRowView: View {
|
||||
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@ObservedObject var viewModel: EpisodesRowViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
HStack {
|
||||
Menu {
|
||||
ForEach(Array(viewModel.seasonsEpisodes.keys).sorted(by: { $0.name ?? "" < $1.name ?? ""}), id:\.self) { season in
|
||||
Button {
|
||||
viewModel.selectedSeason = season
|
||||
} label: {
|
||||
if season.id == viewModel.selectedSeason?.id {
|
||||
Label(season.name ?? "Season", systemImage: "checkmark")
|
||||
} else {
|
||||
Text(season.name ?? "Season")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Text(viewModel.selectedSeason?.name ?? "Unknown")
|
||||
.fontWeight(.semibold)
|
||||
.fixedSize()
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
ScrollViewReader { reader in
|
||||
HStack(alignment: .top, spacing: 15) {
|
||||
if viewModel.isLoading {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
ZStack {
|
||||
Color.gray.ignoresSafeArea()
|
||||
|
||||
ProgressView()
|
||||
}
|
||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||
.frame(width: 200, height: 112)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("--")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Loading")
|
||||
.font(.body)
|
||||
.padding(.bottom, 1)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 200)
|
||||
} else if let selectedSeason = viewModel.selectedSeason {
|
||||
if viewModel.seasonsEpisodes[selectedSeason]!.isEmpty {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Color.gray.ignoresSafeArea()
|
||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||
.frame(width: 200, height: 112)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("--")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No episodes available")
|
||||
.font(.body)
|
||||
.padding(.bottom, 1)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 200)
|
||||
} else {
|
||||
ForEach(viewModel.seasonsEpisodes[selectedSeason]!, id:\.self) { episode in
|
||||
Button {
|
||||
itemRouter.route(to: \.item, episode)
|
||||
} label: {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
ImageView(src: episode.getBackdropImage(maxWidth: 200),
|
||||
bh: episode.getBackdropImageBlurHash())
|
||||
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
|
||||
.frame(width: 200, height: 112)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(episode.getEpisodeLocator() ?? "")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Text(episode.name ?? "")
|
||||
.font(.body)
|
||||
.padding(.bottom, 1)
|
||||
.lineLimit(2)
|
||||
Text(episode.overview ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
.lineLimit(3)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 200)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.id(episode.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.onChange(of: viewModel.selectedSeason) { _ in
|
||||
if viewModel.selectedSeason?.id == viewModel.episodeItemViewModel.item.seasonId {
|
||||
reader.scrollTo(viewModel.episodeItemViewModel.item.name)
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.seasonsEpisodes) { _ in
|
||||
if viewModel.selectedSeason?.id == viewModel.episodeItemViewModel.item.seasonId {
|
||||
reader.scrollTo(viewModel.episodeItemViewModel.item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,11 @@ struct ItemNavigationView: View {
|
|||
|
||||
var body: some View {
|
||||
ItemView(item: item)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
.navigationBarTitle(item.name ?? "", displayMode: .inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.clear]
|
||||
navigationController.navigationBar.titleTextAttributes = textAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,21 +64,6 @@ private struct ItemView: View {
|
|||
} label: {
|
||||
Image(systemName: "ellipsis.circle.fill")
|
||||
}
|
||||
case .episode:
|
||||
Menu {
|
||||
Button {
|
||||
(viewModel as? EpisodeItemViewModel)?.routeToSeriesItem()
|
||||
} label: {
|
||||
Label("Show Series", systemImage: "text.below.photo")
|
||||
}
|
||||
Button {
|
||||
(viewModel as? EpisodeItemViewModel)?.routeToSeasonItem()
|
||||
} label: {
|
||||
Label("Show Season", systemImage: "square.fill.text.grid.1x2")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle.fill")
|
||||
}
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ItemViewBody: View {
|
||||
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
@Default(.showCastAndCrew) var showCastAndCrew
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -27,13 +30,11 @@ struct ItemViewBody: View {
|
|||
|
||||
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
||||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
||||
maxWidth: 150,
|
||||
topBarView: {
|
||||
L10n.seasons.text
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
}, selectedAction: { season in
|
||||
itemRouter.route(to: \.item, season)
|
||||
})
|
||||
|
@ -46,6 +47,7 @@ struct ItemViewBody: View {
|
|||
selectedAction: { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
})
|
||||
.padding(.bottom)
|
||||
|
||||
// MARK: Studios
|
||||
|
||||
|
@ -53,42 +55,66 @@ struct ItemViewBody: View {
|
|||
PillHStackView(title: L10n.studios,
|
||||
items: studios) { studio in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
// MARK: Episodes
|
||||
|
||||
if let episodeViewModel = viewModel as? EpisodeItemViewModel {
|
||||
EpisodesRowView(viewModel: EpisodesRowViewModel(episodeItemViewModel: episodeViewModel))
|
||||
}
|
||||
|
||||
if let episodeViewModel = viewModel as? EpisodeItemViewModel {
|
||||
if let seriesItem = episodeViewModel.series {
|
||||
let a = [seriesItem]
|
||||
PortraitImageHStackView(items: a) {
|
||||
Text("Series")
|
||||
.fontWeight(.semibold)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
} selectedAction: { seriesItem in
|
||||
itemRouter.route(to: \.item, seriesItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Cast & Crew
|
||||
|
||||
if let castAndCrew = viewModel.item.people {
|
||||
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
||||
maxWidth: 150,
|
||||
topBarView: {
|
||||
Text("Cast & Crew")
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
},
|
||||
selectedAction: { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
})
|
||||
if showCastAndCrew {
|
||||
if let castAndCrew = viewModel.item.people {
|
||||
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
||||
topBarView: {
|
||||
Text("Cast & Crew")
|
||||
.fontWeight(.semibold)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
},
|
||||
selectedAction: { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: More Like This
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitImageHStackView(items: viewModel.similarItems,
|
||||
maxWidth: 150,
|
||||
topBarView: {
|
||||
L10n.moreLikeThis.text
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
},
|
||||
selectedAction: { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Details
|
||||
|
||||
ItemViewDetailsView(viewModel: viewModel)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ItemViewDetailsView: View {
|
||||
|
||||
@ObservedObject var viewModel: ItemViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
if !viewModel.informationItems.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Information")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
|
||||
ForEach(viewModel.informationItems, id: \.self.title) { informationItem in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(informationItem.title)
|
||||
.font(.subheadline)
|
||||
Text(informationItem.content)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
if !viewModel.mediaItems.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Media")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
|
||||
ForEach(viewModel.mediaItems, id: \.self.title) { mediaItem in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(mediaItem.title)
|
||||
.font(.subheadline)
|
||||
Text(mediaItem.content)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,21 +8,18 @@
|
|||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LatestMediaView: View {
|
||||
struct LatestMediaView<TopBarView: View>: View {
|
||||
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@StateObject var viewModel: LatestMediaViewModel
|
||||
var topBarView: () -> TopBarView
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}.padding(.leading, 20)
|
||||
}.frame(height: 200)
|
||||
PortraitImageHStackView(items: viewModel.items,
|
||||
horizontalAlignment: .leading) {
|
||||
topBarView()
|
||||
} selectedAction: { item in
|
||||
homeRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,27 +38,6 @@ struct LibraryListView: View {
|
|||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
NavigationLink(destination: LazyView {
|
||||
L10n.wip.text
|
||||
}) {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
L10n.allGenres.text
|
||||
.foregroundColor(.black)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.white)
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 15)
|
||||
|
||||
if !viewModel.isLoading {
|
||||
ForEach(viewModel.libraries, id: \.id) { library in
|
||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/* JellyfinPlayer/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
|
||||
|
||||
struct LoadingView<Content>: View where Content: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var isShowing: Bool // should the modal be visible?
|
||||
var content: () -> Content
|
||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { _ in
|
||||
ZStack(alignment: .center) {
|
||||
// the content to display - if the modal is showing, we'll blur it
|
||||
content()
|
||||
.disabled(isShowing)
|
||||
.blur(radius: isShowing ? 2 : 0)
|
||||
|
||||
// all contents inside here will only be shown when isShowing is true
|
||||
if isShowing {
|
||||
// this Rectangle is a semi-transparent black overlay
|
||||
Rectangle()
|
||||
.fill(Color.black).opacity(isShowing ? 0.6 : 0)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
// the magic bit - our ProgressView just displays an activity
|
||||
// indicator, with some text underneath showing what we are doing
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? L10n.loading).fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10))
|
||||
.frame(width: 250)
|
||||
.background(colorScheme == .dark ? Color(UIColor.systemGray6) : Color.white)
|
||||
.foregroundColor(Color.primary)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadingViewNoBlur<Content>: View where Content: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var isShowing: Bool // should the modal be visible?
|
||||
var content: () -> Content
|
||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { _ in
|
||||
ZStack(alignment: .center) {
|
||||
// the content to display - if the modal is showing, we'll blur it
|
||||
content()
|
||||
.disabled(isShowing)
|
||||
|
||||
// all contents inside here will only be shown when isShowing is true
|
||||
if isShowing {
|
||||
// this Rectangle is a semi-transparent black overlay
|
||||
Rectangle()
|
||||
.fill(Color.black).opacity(isShowing ? 0.6 : 0)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
// the magic bit - our ProgressView just displays an activity
|
||||
// indicator, with some text underneath showing what we are doing
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? L10n.loading).fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
}
|
||||
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10))
|
||||
.frame(width: 250)
|
||||
.background(colorScheme == .dark ? Color(UIColor.systemGray6) : Color.white)
|
||||
.foregroundColor(Color.primary)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ struct OverlaySettingsView: View {
|
|||
Toggle("\(Image(systemName: "chevron.left.circle")) Play Previous Item", isOn: $shouldShowPlayPreviousItem)
|
||||
Toggle("\(Image(systemName: "chevron.right.circle")) Play Next Item", isOn: $shouldShowPlayNextItem)
|
||||
Toggle("\(Image(systemName: "play.circle.fill")) Auto Play", isOn: $shouldShowAutoPlay)
|
||||
Toggle("Allow Edit Jump Lengths", isOn: $shouldShowJumpButtonsInOverlayMenu)
|
||||
Toggle("Edit Jump Lengths", isOn: $shouldShowJumpButtonsInOverlayMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ struct SettingsView: View {
|
|||
@Default(.videoPlayerJumpForward) var jumpForwardLength
|
||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||
@Default(.jumpGesturesEnabled) var jumpGesturesEnabled
|
||||
@Default(.showPosterLabels) var showPosterLabels
|
||||
@Default(.showCastAndCrew) var showCastAndCrew
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
@ -114,26 +116,9 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section(header: L10n.accessibility.text) {
|
||||
// Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||
// SearchablePicker(label: "Preferred subtitle language",
|
||||
// options: viewModel.langs,
|
||||
// optionToString: { $0.name },
|
||||
// selected: Binding<TrackLanguage>(get: {
|
||||
// viewModel.langs
|
||||
// .first(where: { $0.isoCode == autoSelectSubtitlesLangcode
|
||||
// }) ??
|
||||
// .auto
|
||||
// },
|
||||
// set: { autoSelectSubtitlesLangcode = $0.isoCode }))
|
||||
// SearchablePicker(label: "Preferred audio language",
|
||||
// options: viewModel.langs,
|
||||
// optionToString: { $0.name },
|
||||
// selected: Binding<TrackLanguage>(get: {
|
||||
// viewModel.langs
|
||||
// .first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
|
||||
// .auto
|
||||
// },
|
||||
// set: { autoSelectAudioLangcode = $0.isoCode }))
|
||||
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||
Toggle("Show Cast and Crew", isOn: $showCastAndCrew)
|
||||
|
||||
Picker(L10n.appearance, selection: $appAppearance) {
|
||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
|
|
|
@ -84,10 +84,9 @@ class VLCPlayerViewController: UIViewController {
|
|||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
let defaultNotificationCenter = NotificationCenter.default
|
||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
|
||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
|
||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didEndPlayback, object: nil)
|
||||
}
|
||||
|
||||
// MARK: viewDidLoad
|
||||
|
|
|
@ -7,24 +7,36 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// MARK: PortraitImageStackable
|
||||
extension BaseItemDto: PortraitImageStackable {
|
||||
public var portraitImageID: String {
|
||||
return id ?? "no id"
|
||||
}
|
||||
|
||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||
return self.getPrimaryImage(maxWidth: maxWidth)
|
||||
switch self.itemType {
|
||||
case .episode:
|
||||
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
||||
default:
|
||||
return self.getPrimaryImage(maxWidth: maxWidth)
|
||||
}
|
||||
}
|
||||
|
||||
public var title: String {
|
||||
return self.name ?? ""
|
||||
switch self.itemType {
|
||||
case .episode:
|
||||
return self.seriesName ?? self.name ?? ""
|
||||
default:
|
||||
return self.name ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String? {
|
||||
public var subtitle: String? {
|
||||
switch self.itemType {
|
||||
case .season:
|
||||
guard let productionYear = productionYear else { return nil }
|
||||
return "\(productionYear)"
|
||||
case .episode:
|
||||
return getEpisodeLocator()
|
||||
default:
|
||||
|
@ -41,4 +53,13 @@ extension BaseItemDto: PortraitImageStackable {
|
|||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||
return String(initials)
|
||||
}
|
||||
|
||||
public var showTitle: Bool {
|
||||
switch self.itemType {
|
||||
case .episode, .series, .movie:
|
||||
return Defaults[.showPosterLabels]
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,11 @@ extension BaseItemDto {
|
|||
|
||||
var subtitle: String? = nil
|
||||
|
||||
// MARK: Attach media content to self
|
||||
|
||||
var modifiedSelfItem = self
|
||||
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
|
||||
|
||||
// TODO: other forms of media subtitle
|
||||
if self.itemType == .episode {
|
||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||
|
@ -101,8 +106,8 @@ extension BaseItemDto {
|
|||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||
|
||||
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
|
||||
title: self.name ?? "",
|
||||
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
||||
title: modifiedSelfItem.name ?? "",
|
||||
subtitle: subtitle,
|
||||
streamURL: streamURL.url!,
|
||||
hlsURL: hlsURL.url!,
|
||||
|
|
|
@ -156,9 +156,9 @@ public extension BaseItemDto {
|
|||
return text
|
||||
}
|
||||
|
||||
func getItemProgressString() -> String {
|
||||
func getItemProgressString() -> String? {
|
||||
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
|
||||
|
@ -208,4 +208,60 @@ public extension BaseItemDto {
|
|||
return getPrimaryImage(maxWidth: maxWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ItemDetail
|
||||
|
||||
struct ItemDetail {
|
||||
let title: String
|
||||
let content: String
|
||||
}
|
||||
|
||||
func createInformationItems() -> [ItemDetail] {
|
||||
var informationItems: [ItemDetail] = []
|
||||
|
||||
if let productionYear = productionYear {
|
||||
informationItems.append(ItemDetail(title: "Released", content: "\(productionYear)"))
|
||||
}
|
||||
|
||||
if let rating = officialRating {
|
||||
informationItems.append(ItemDetail(title: "Rated", content: "\(rating)"))
|
||||
}
|
||||
|
||||
if let runtime = getItemRuntime() {
|
||||
informationItems.append(ItemDetail(title: "Runtime", content: runtime))
|
||||
}
|
||||
|
||||
return informationItems
|
||||
}
|
||||
|
||||
func createMediaItems() -> [ItemDetail] {
|
||||
var mediaItems: [ItemDetail] = []
|
||||
|
||||
if let container = container {
|
||||
let containerList = container.split(separator: ",").joined(separator: ", ")
|
||||
|
||||
if containerList.count > 1 {
|
||||
mediaItems.append(ItemDetail(title: "Containers", content: containerList))
|
||||
} else {
|
||||
mediaItems.append(ItemDetail(title: "Container", content: containerList))
|
||||
}
|
||||
}
|
||||
|
||||
if let mediaStreams = mediaStreams {
|
||||
let audioStreams = mediaStreams.filter({ $0.type == .audio })
|
||||
let subtitleStreams = mediaStreams.filter({ $0.type == .subtitle })
|
||||
|
||||
if !audioStreams.isEmpty {
|
||||
let audioList = audioStreams.compactMap({ $0.displayTitle }).joined(separator: ", ")
|
||||
mediaItems.append(ItemDetail(title: "Audio", content: audioList))
|
||||
}
|
||||
|
||||
if !subtitleStreams.isEmpty {
|
||||
let subtitleList = subtitleStreams.compactMap({ "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }).joined(separator: ", ")
|
||||
mediaItems.append(ItemDetail(title: "Subtitles", content: subtitleList))
|
||||
}
|
||||
}
|
||||
|
||||
return mediaItems
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,10 @@ extension BaseItemPerson {
|
|||
|
||||
// MARK: PortraitImageStackable
|
||||
extension BaseItemPerson: PortraitImageStackable {
|
||||
public var portraitImageID: String {
|
||||
return (id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials
|
||||
}
|
||||
|
||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
|
||||
}
|
||||
|
@ -68,7 +72,7 @@ extension BaseItemPerson: PortraitImageStackable {
|
|||
return self.name ?? ""
|
||||
}
|
||||
|
||||
public var description: String? {
|
||||
public var subtitle: String? {
|
||||
return self.firstRole()
|
||||
}
|
||||
|
||||
|
@ -81,6 +85,10 @@ extension BaseItemPerson: PortraitImageStackable {
|
|||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||
return String(initials)
|
||||
}
|
||||
|
||||
public var showTitle: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: DiplayedType
|
||||
|
|
|
@ -12,7 +12,9 @@ import Foundation
|
|||
public protocol PortraitImageStackable {
|
||||
func imageURLContsructor(maxWidth: Int) -> URL
|
||||
var title: String { get }
|
||||
var description: String? { get }
|
||||
var subtitle: String? { get }
|
||||
var blurHash: String { get }
|
||||
var failureInitials: String { get }
|
||||
var portraitImageID: String { get }
|
||||
var showTitle: Bool { get }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
/*
|
||||
* 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
|
||||
|
||||
enum PosterSize {
|
||||
case small
|
||||
case normal
|
||||
}
|
|
@ -21,5 +21,7 @@ enum SwiftfinNotificationCenter {
|
|||
static let processDeepLink = Notification.Name("processDeepLink")
|
||||
static let didPurge = Notification.Name("didPurge")
|
||||
static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI")
|
||||
|
||||
static let didEndPlayback = Notification.Name("didEndPlayback")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,11 @@ extension Defaults.Keys {
|
|||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.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)
|
||||
|
||||
// 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 videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
|
|
@ -15,12 +15,12 @@ import Stinsen
|
|||
final class EpisodeItemViewModel: ItemViewModel {
|
||||
|
||||
@RouterObject var itemRouter: ItemCoordinator.Router?
|
||||
var seasonEpisodes: [BaseItemDto] = []
|
||||
@Published var series: BaseItemDto?
|
||||
|
||||
override init(item: BaseItemDto) {
|
||||
super.init(item: item)
|
||||
|
||||
getSeasonEpisodes()
|
||||
getEpisodeSeries()
|
||||
}
|
||||
|
||||
override func getItemDisplayName() -> String {
|
||||
|
@ -32,41 +32,15 @@ final class EpisodeItemViewModel: ItemViewModel {
|
|||
return false
|
||||
}
|
||||
|
||||
func routeToSeasonItem() {
|
||||
guard let id = item.seasonId else { return }
|
||||
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
}, receiveValue: { [weak self] item in
|
||||
self?.itemRouter?.route(to: \.item, item)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func routeToSeriesItem() {
|
||||
func getEpisodeSeries() {
|
||||
guard let id = item.seriesId else { return }
|
||||
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
}, receiveValue: { [weak self] item in
|
||||
self?.itemRouter?.route(to: \.item, item)
|
||||
self?.series = item
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func getSeasonEpisodes() {
|
||||
guard let seriesID = item.seriesId else { return }
|
||||
TvShowsAPI.getEpisodes(seriesId: seriesID,
|
||||
userId: SessionManager.main.currentLogin.user.id,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||
seasonId: item.seasonId ?? "")
|
||||
.sink { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { [weak self] item in
|
||||
self?.seasonEpisodes = item.items ?? []
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
/*
|
||||
* 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 JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
final class EpisodesRowViewModel: ViewModel {
|
||||
|
||||
@ObservedObject var episodeItemViewModel: EpisodeItemViewModel
|
||||
@Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
|
||||
@Published var selectedSeason: BaseItemDto? {
|
||||
willSet {
|
||||
if seasonsEpisodes[newValue!]!.isEmpty {
|
||||
retrieveEpisodesForSeason(newValue!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(episodeItemViewModel: EpisodeItemViewModel) {
|
||||
self.episodeItemViewModel = episodeItemViewModel
|
||||
super.init()
|
||||
|
||||
retrieveSeasons()
|
||||
}
|
||||
|
||||
private func retrieveSeasons() {
|
||||
TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "",
|
||||
userId: SessionManager.main.currentLogin.user.id)
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { response in
|
||||
let seasons = response.items ?? []
|
||||
seasons.forEach { season in
|
||||
self.seasonsEpisodes[season] = []
|
||||
|
||||
if season.id == self.episodeItemViewModel.item.seasonId ?? "" {
|
||||
self.selectedSeason = season
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func retrieveEpisodesForSeason(_ season: BaseItemDto) {
|
||||
guard let seasonID = season.id else { return }
|
||||
|
||||
TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "",
|
||||
userId: SessionManager.main.currentLogin.user.id,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||
seasonId: seasonID)
|
||||
.trackActivity(loading)
|
||||
.sink { completion in
|
||||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { episodes in
|
||||
self.seasonsEpisodes[season] = episodes.items ?? []
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -32,9 +32,14 @@ final class HomeViewModel: ViewModel {
|
|||
let nc = SwiftfinNotificationCenter.main
|
||||
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||
nc.addObserver(self, selector: #selector(didEndPlayback), name: SwiftfinNotificationCenter.Keys.didEndPlayback, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
SwiftfinNotificationCenter.main.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc func didSignIn() {
|
||||
@objc private func didSignIn() {
|
||||
for cancellable in cancellables {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
@ -47,16 +52,29 @@ final class HomeViewModel: ViewModel {
|
|||
refresh()
|
||||
}
|
||||
|
||||
@objc func didSignOut() {
|
||||
@objc private func didSignOut() {
|
||||
for cancellable in cancellables {
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
@objc private func didEndPlayback() {
|
||||
refreshResumeItems()
|
||||
refreshNextUpItems()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
@objc func refresh() {
|
||||
LogManager.shared.log.debug("Refresh called.")
|
||||
|
||||
refreshLibrariesLatest()
|
||||
refreshResumeItems()
|
||||
refreshNextUpItems()
|
||||
}
|
||||
|
||||
// MARK: Libraries Latest Items
|
||||
private func refreshLibrariesLatest() {
|
||||
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
|
@ -101,7 +119,10 @@ final class HomeViewModel: ViewModel {
|
|||
.store(in: &self.cancellables)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
}
|
||||
|
||||
// MARK: Resume Items
|
||||
private func refreshResumeItems() {
|
||||
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
||||
mediaTypes: ["Video"],
|
||||
|
@ -121,7 +142,10 @@ final class HomeViewModel: ViewModel {
|
|||
self.resumeItems = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
}
|
||||
|
||||
// MARK: Next Up Items
|
||||
private func refreshNextUpItems() {
|
||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters])
|
||||
.trackActivity(loading)
|
||||
|
|
|
@ -19,6 +19,8 @@ class ItemViewModel: ViewModel {
|
|||
@Published var similarItems: [BaseItemDto] = []
|
||||
@Published var isWatched = false
|
||||
@Published var isFavorited = false
|
||||
@Published var informationItems: [BaseItemDto.ItemDetail]
|
||||
@Published var mediaItems: [BaseItemDto.ItemDetail]
|
||||
var itemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
|
@ -29,6 +31,9 @@ class ItemViewModel: ViewModel {
|
|||
self.playButtonItem = item
|
||||
default: ()
|
||||
}
|
||||
|
||||
informationItems = item.createInformationItems()
|
||||
mediaItems = item.createMediaItems()
|
||||
|
||||
isFavorited = item.userData?.isFavorite ?? false
|
||||
isWatched = item.userData?.played ?? false
|
||||
|
@ -41,12 +46,17 @@ class ItemViewModel: ViewModel {
|
|||
self.handleAPIRequestError(completion: completion)
|
||||
} receiveValue: { videoPlayerViewModel in
|
||||
self.itemVideoPlayerViewModel = videoPlayerViewModel
|
||||
self.mediaItems = videoPlayerViewModel.item.createMediaItems()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func playButtonText() -> String {
|
||||
return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString()
|
||||
if let itemProgressString = item.getItemProgressString() {
|
||||
return itemProgressString
|
||||
}
|
||||
|
||||
return L10n.play
|
||||
}
|
||||
|
||||
func getItemDisplayName() -> String {
|
||||
|
|
Loading…
Reference in New Issue