iOS advanced seasons episodes selection
This commit is contained in:
parent
1f199dc4f8
commit
78c061d4de
|
@ -12,55 +12,8 @@ import SwiftUI
|
||||||
struct ItemDetailsView: View {
|
struct ItemDetailsView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: ItemViewModel
|
@ObservedObject var viewModel: ItemViewModel
|
||||||
private let detailItems: [(String, String)]
|
|
||||||
private let mediaItems: [(String, String)]
|
|
||||||
@FocusState private var focused: Bool
|
@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 {
|
var body: some View {
|
||||||
|
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
|
|
|
@ -13,9 +13,10 @@ struct LatestMediaView: View {
|
||||||
|
|
||||||
@StateObject var tempViewModel = ViewModel()
|
@StateObject var tempViewModel = ViewModel()
|
||||||
@State var items: [BaseItemDto] = []
|
@State var items: [BaseItemDto] = []
|
||||||
private var library_id: String = ""
|
|
||||||
@State private var viewDidLoad: Bool = false
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
|
private var library_id: String = ""
|
||||||
|
|
||||||
init(usingParentID: String) {
|
init(usingParentID: String) {
|
||||||
library_id = usingParentID
|
library_id = usingParentID
|
||||||
}
|
}
|
||||||
|
@ -26,7 +27,6 @@ struct LatestMediaView: View {
|
||||||
}
|
}
|
||||||
viewDidLoad = true
|
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)
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
|
@ -35,7 +35,6 @@ struct LatestMediaView: View {
|
||||||
})
|
})
|
||||||
.store(in: &tempViewModel.cancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
|
|
@ -76,7 +76,6 @@
|
||||||
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
||||||
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.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 */; };
|
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
||||||
53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
||||||
53913BF026D323FE00EB3286 /* 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 */; };
|
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.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 */; };
|
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA44277BB646000269ED /* JellyfinAPI */; };
|
||||||
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA46277BB670000269ED /* JellyfinAPI */; };
|
||||||
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
E10EAA4D277BB716000269ED /* Sliders in Frameworks */ = {isa = PBXBuildFile; productRef = E10EAA4C277BB716000269ED /* Sliders */; };
|
||||||
|
@ -493,7 +499,6 @@
|
||||||
5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||||
|
@ -767,6 +776,7 @@
|
||||||
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
|
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
|
||||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||||
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */,
|
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */,
|
||||||
|
E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */,
|
||||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||||
62E632F2267D54030063E547 /* ItemViewModel.swift */,
|
62E632F2267D54030063E547 /* ItemViewModel.swift */,
|
||||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
|
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
|
||||||
|
@ -891,6 +901,7 @@
|
||||||
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
E1AA331E2782639D00F6439C /* OverlayType.swift */,
|
||||||
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
|
||||||
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
|
||||||
|
E10D87DD278510E300BD264C /* PosterSize.swift */,
|
||||||
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
|
||||||
535870AC2669D8DD00D05A09 /* Typings.swift */,
|
535870AC2669D8DD00D05A09 /* Typings.swift */,
|
||||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
||||||
|
@ -1299,7 +1310,6 @@
|
||||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||||
53892771263C8C6F0035E14B /* LoadingView.swift */,
|
|
||||||
5389276F263C25230035E14B /* NextUpView.swift */,
|
5389276F263C25230035E14B /* NextUpView.swift */,
|
||||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||||
|
@ -1315,7 +1325,9 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
||||||
|
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
|
||||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */,
|
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */,
|
||||||
|
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */,
|
||||||
E18845FB26DEACC400B0C5B7 /* Landscape */,
|
E18845FB26DEACC400B0C5B7 /* Landscape */,
|
||||||
E18845FA26DEACBE00B0C5B7 /* Portrait */,
|
E18845FA26DEACBE00B0C5B7 /* Portrait */,
|
||||||
);
|
);
|
||||||
|
@ -1955,6 +1967,7 @@
|
||||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||||
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
||||||
|
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */,
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
||||||
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
|
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
|
||||||
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
||||||
|
@ -2020,6 +2033,7 @@
|
||||||
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
|
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
|
||||||
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
|
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
|
||||||
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
|
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
|
||||||
|
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
||||||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||||
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
||||||
|
@ -2066,6 +2080,7 @@
|
||||||
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
E1C812C0277A8E5D00918266 /* VLCPlayerView.swift in Sources */,
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||||
|
E10D87E227852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,
|
||||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
|
@ -2100,6 +2115,7 @@
|
||||||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
|
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */,
|
||||||
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
|
E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */,
|
||||||
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
|
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */,
|
||||||
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */,
|
||||||
|
@ -2125,6 +2141,7 @@
|
||||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
|
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */,
|
||||||
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
|
||||||
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
|
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
|
@ -2172,10 +2189,10 @@
|
||||||
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
||||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||||
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
|
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
|
||||||
|
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */,
|
||||||
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
||||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
|
||||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -2195,6 +2212,7 @@
|
||||||
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
|
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
|
||||||
6264E88E273850380081A12A /* Strings.swift in Sources */,
|
6264E88E273850380081A12A /* Strings.swift in Sources */,
|
||||||
|
E10D87E0278510E400BD264C /* PosterSize.swift in Sources */,
|
||||||
E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
||||||
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
||||||
|
|
|
@ -60,7 +60,6 @@ struct EpisodeCardVStackView: View {
|
||||||
.overlay(
|
.overlay(
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.jellyfinPurple)
|
.fill(Color.jellyfinPurple)
|
||||||
.mask(ProgressBar())
|
|
||||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 1.5), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,6 @@ struct PillHStackView<ItemType: PillStackable>: View {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
// let navigationView: (ItemType) -> NavigationView
|
|
||||||
let selectedAction: (ItemType) -> Void
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -12,66 +12,70 @@ import SwiftUI
|
||||||
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||||
|
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
let maxWidth: Int
|
let maxWidth: CGFloat
|
||||||
let horizontalAlignment: HorizontalAlignment
|
let horizontalAlignment: HorizontalAlignment
|
||||||
|
let textAlignment: TextAlignment
|
||||||
let topBarView: () -> TopBarView
|
let topBarView: () -> TopBarView
|
||||||
let selectedAction: (ItemType) -> Void
|
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.items = items
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.horizontalAlignment = horizontalAlignment
|
self.horizontalAlignment = horizontalAlignment
|
||||||
|
self.textAlignment = textAlignment
|
||||||
self.topBarView = topBarView
|
self.topBarView = topBarView
|
||||||
self.selectedAction = selectedAction
|
self.selectedAction = selectedAction
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
topBarView()
|
topBarView()
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
VStack {
|
HStack(alignment: .top, spacing: 15) {
|
||||||
Spacer().frame(height: 8)
|
ForEach(items, id: \.self.portraitImageID) { item in
|
||||||
HStack(alignment: .top) {
|
|
||||||
|
|
||||||
Spacer().frame(width: 16)
|
|
||||||
|
|
||||||
ForEach(items, id: \.title) { item in
|
|
||||||
Button {
|
Button {
|
||||||
selectedAction(item)
|
selectedAction(item)
|
||||||
} label: {
|
} label: {
|
||||||
VStack {
|
VStack(alignment: horizontalAlignment) {
|
||||||
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
|
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
|
||||||
bh: item.blurHash,
|
bh: item.blurHash,
|
||||||
failureInitials: item.failureInitials)
|
failureInitials: item.failureInitials)
|
||||||
.frame(width: 100, height: CGFloat(maxWidth))
|
.frame(width: maxWidth, height: maxWidth * 1.5)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
|
|
||||||
|
if item.showTitle {
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.fontWeight(.regular)
|
.fontWeight(.regular)
|
||||||
.frame(width: 100)
|
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(textAlignment)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
if let description = item.description {
|
if let description = item.subtitle {
|
||||||
Text(description)
|
Text(description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.frame(width: 100)
|
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(textAlignment)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.frame(width: maxWidth)
|
||||||
}
|
}
|
||||||
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding(.horizontal)
|
||||||
}.padding(.top, -3)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ struct PortraitItemView: View {
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.overlay(Rectangle()
|
.overlay(Rectangle()
|
||||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||||
.mask(ProgressBar())
|
|
||||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading)
|
.padding(0), alignment: .bottomLeading)
|
||||||
.overlay(ZStack {
|
.overlay(ZStack {
|
||||||
|
|
|
@ -9,71 +9,79 @@
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
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 {
|
struct ContinueWatchingView: View {
|
||||||
|
|
||||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
var items: [BaseItemDto]
|
@ObservedObject var viewModel: HomeViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack {
|
HStack(alignment: .top, spacing: 20) {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(viewModel.resumeItems, id: \.id) { item in
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
homeRouter.route(to: \.item, item)
|
homeRouter.route(to: \.item, item)
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
ZStack {
|
||||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||||
.frame(width: 320, height: 180)
|
.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 {
|
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 ?? "")")
|
Text("\(item.seriesName ?? item.name ?? "")")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if item.type == "Episode" {
|
|
||||||
Text("• \(item.getEpisodeLocator() ?? "") - \(item.name ?? "")")
|
if item.itemType == .episode {
|
||||||
|
Text(item.getEpisodeLocator() ?? "")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.offset(x: -1.4)
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
}
|
||||||
}.frame(width: 320, alignment: .leading)
|
}
|
||||||
}.padding(.top, 10)
|
}
|
||||||
.padding(.bottom, 5)
|
}
|
||||||
}
|
.padding(.horizontal)
|
||||||
}.padding(.trailing, 16)
|
|
||||||
}.frame(height: 215)
|
|
||||||
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,18 +51,31 @@ struct HomeView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if !viewModel.resumeItems.isEmpty {
|
if !viewModel.resumeItems.isEmpty {
|
||||||
ContinueWatchingView(items: viewModel.resumeItems)
|
ContinueWatchingView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
if !viewModel.nextUpItems.isEmpty {
|
if !viewModel.nextUpItems.isEmpty {
|
||||||
NextUpView(items: viewModel.nextUpItems)
|
PortraitImageHStackView(items: viewModel.nextUpItems,
|
||||||
|
horizontalAlignment: .leading) {
|
||||||
|
L10n.nextUp.text
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding()
|
||||||
|
} selectedAction: { item in
|
||||||
|
homeRouter.route(to: \.item, item)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(viewModel.libraries, id: \.self) { library in
|
ForEach(viewModel.libraries, id: \.self) { library in
|
||||||
|
|
||||||
|
LatestMediaView(viewModel: LatestMediaViewModel(libraryID: library.id!)) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(L10n.latestWithString(library.name ?? ""))
|
Text(L10n.latestWithString(library.name ?? ""))
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
homeRouter
|
homeRouter
|
||||||
.route(to: \.library, (viewModel: .init(parentID: library.id!,
|
.route(to: \.library, (viewModel: .init(parentID: library.id!,
|
||||||
|
@ -74,12 +87,13 @@ struct HomeView: View {
|
||||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.padding(.leading, 16)
|
}
|
||||||
.padding(.trailing, 16)
|
.padding()
|
||||||
LatestMediaView(viewModel: .init(libraryID: library.id!))
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
.padding(.bottom, 50)
|
||||||
}
|
}
|
||||||
.introspectScrollView { scrollView in
|
.introspectScrollView { scrollView in
|
||||||
let control = UIRefreshControl()
|
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 {
|
var body: some View {
|
||||||
ItemView(item: item)
|
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: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle.fill")
|
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:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,15 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ItemViewBody: View {
|
struct ItemViewBody: View {
|
||||||
|
|
||||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
|
@Default(.showCastAndCrew) var showCastAndCrew
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -27,13 +30,11 @@ struct ItemViewBody: View {
|
||||||
|
|
||||||
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
||||||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
PortraitImageHStackView(items: seriesViewModel.seasons,
|
||||||
maxWidth: 150,
|
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.seasons.text
|
L10n.seasons.text
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.top, 3)
|
.padding(.bottom)
|
||||||
.padding(.leading, 16)
|
.padding(.horizontal)
|
||||||
}, selectedAction: { season in
|
}, selectedAction: { season in
|
||||||
itemRouter.route(to: \.item, season)
|
itemRouter.route(to: \.item, season)
|
||||||
})
|
})
|
||||||
|
@ -46,6 +47,7 @@ struct ItemViewBody: View {
|
||||||
selectedAction: { genre in
|
selectedAction: { genre in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||||
})
|
})
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
// MARK: Studios
|
// MARK: Studios
|
||||||
|
|
||||||
|
@ -53,42 +55,66 @@ struct ItemViewBody: View {
|
||||||
PillHStackView(title: L10n.studios,
|
PillHStackView(title: L10n.studios,
|
||||||
items: studios) { studio in
|
items: studios) { studio in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
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
|
// MARK: Cast & Crew
|
||||||
|
|
||||||
|
if showCastAndCrew {
|
||||||
if let castAndCrew = viewModel.item.people {
|
if let castAndCrew = viewModel.item.people {
|
||||||
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
||||||
maxWidth: 150,
|
|
||||||
topBarView: {
|
topBarView: {
|
||||||
Text("Cast & Crew")
|
Text("Cast & Crew")
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.top, 3)
|
.padding(.bottom)
|
||||||
.padding(.leading, 16)
|
.padding(.horizontal)
|
||||||
},
|
},
|
||||||
selectedAction: { person in
|
selectedAction: { person in
|
||||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: More Like This
|
// MARK: More Like This
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitImageHStackView(items: viewModel.similarItems,
|
PortraitImageHStackView(items: viewModel.similarItems,
|
||||||
maxWidth: 150,
|
|
||||||
topBarView: {
|
topBarView: {
|
||||||
L10n.moreLikeThis.text
|
L10n.moreLikeThis.text
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.top, 3)
|
.padding(.bottom)
|
||||||
.padding(.leading, 16)
|
.padding(.horizontal)
|
||||||
},
|
},
|
||||||
selectedAction: { item in
|
selectedAction: { item in
|
||||||
itemRouter.route(to: \.item, item)
|
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 Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LatestMediaView: View {
|
struct LatestMediaView<TopBarView: View>: View {
|
||||||
|
|
||||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
@StateObject var viewModel: LatestMediaViewModel
|
@StateObject var viewModel: LatestMediaViewModel
|
||||||
|
var topBarView: () -> TopBarView
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
PortraitImageHStackView(items: viewModel.items,
|
||||||
LazyHStack {
|
horizontalAlignment: .leading) {
|
||||||
ForEach(viewModel.items, id: \.id) { item in
|
topBarView()
|
||||||
Button {
|
} selectedAction: { item in
|
||||||
homeRouter.route(to: \.item, item)
|
homeRouter.route(to: \.item, item)
|
||||||
} label: {
|
}
|
||||||
PortraitItemView(item: item)
|
|
||||||
}
|
|
||||||
}.padding(.trailing, 16)
|
|
||||||
}.padding(.leading, 20)
|
|
||||||
}.frame(height: 200)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,27 +38,6 @@ struct LibraryListView: View {
|
||||||
.shadow(radius: 5)
|
.shadow(radius: 5)
|
||||||
.padding(.bottom, 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 {
|
if !viewModel.isLoading {
|
||||||
ForEach(viewModel.libraries, id: \.id) { library in
|
ForEach(viewModel.libraries, id: \.id) { library in
|
||||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
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.left.circle")) Play Previous Item", isOn: $shouldShowPlayPreviousItem)
|
||||||
Toggle("\(Image(systemName: "chevron.right.circle")) Play Next Item", isOn: $shouldShowPlayNextItem)
|
Toggle("\(Image(systemName: "chevron.right.circle")) Play Next Item", isOn: $shouldShowPlayNextItem)
|
||||||
Toggle("\(Image(systemName: "play.circle.fill")) Auto Play", isOn: $shouldShowAutoPlay)
|
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(.videoPlayerJumpForward) var jumpForwardLength
|
||||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||||
@Default(.jumpGesturesEnabled) var jumpGesturesEnabled
|
@Default(.jumpGesturesEnabled) var jumpGesturesEnabled
|
||||||
|
@Default(.showPosterLabels) var showPosterLabels
|
||||||
|
@Default(.showCastAndCrew) var showCastAndCrew
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -114,26 +116,9 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: L10n.accessibility.text) {
|
Section(header: L10n.accessibility.text) {
|
||||||
// Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
Toggle("Show Poster Labels", isOn: $showPosterLabels)
|
||||||
// SearchablePicker(label: "Preferred subtitle language",
|
Toggle("Show Cast and Crew", isOn: $showCastAndCrew)
|
||||||
// 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 }))
|
|
||||||
Picker(L10n.appearance, selection: $appAppearance) {
|
Picker(L10n.appearance, selection: $appAppearance) {
|
||||||
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
ForEach(AppAppearance.allCases, id: \.self) { appearance in
|
||||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||||
|
|
|
@ -84,10 +84,9 @@ class VLCPlayerViewController: UIViewController {
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
let defaultNotificationCenter = NotificationCenter.default
|
NotificationCenter.default.removeObserver(self)
|
||||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
|
|
||||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didEndPlayback, object: nil)
|
||||||
defaultNotificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: viewDidLoad
|
// MARK: viewDidLoad
|
||||||
|
|
|
@ -7,24 +7,36 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
// MARK: PortraitImageStackable
|
// MARK: PortraitImageStackable
|
||||||
extension BaseItemDto: PortraitImageStackable {
|
extension BaseItemDto: PortraitImageStackable {
|
||||||
|
public var portraitImageID: String {
|
||||||
|
return id ?? "no id"
|
||||||
|
}
|
||||||
|
|
||||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||||
|
switch self.itemType {
|
||||||
|
case .episode:
|
||||||
|
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
||||||
|
default:
|
||||||
return self.getPrimaryImage(maxWidth: maxWidth)
|
return self.getPrimaryImage(maxWidth: maxWidth)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var title: String {
|
public var title: String {
|
||||||
|
switch self.itemType {
|
||||||
|
case .episode:
|
||||||
|
return self.seriesName ?? self.name ?? ""
|
||||||
|
default:
|
||||||
return self.name ?? ""
|
return self.name ?? ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var description: String? {
|
public var subtitle: String? {
|
||||||
switch self.itemType {
|
switch self.itemType {
|
||||||
case .season:
|
|
||||||
guard let productionYear = productionYear else { return nil }
|
|
||||||
return "\(productionYear)"
|
|
||||||
case .episode:
|
case .episode:
|
||||||
return getEpisodeLocator()
|
return getEpisodeLocator()
|
||||||
default:
|
default:
|
||||||
|
@ -41,4 +53,13 @@ extension BaseItemDto: PortraitImageStackable {
|
||||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||||
return String(initials)
|
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
|
var subtitle: String? = nil
|
||||||
|
|
||||||
|
// MARK: Attach media content to self
|
||||||
|
|
||||||
|
var modifiedSelfItem = self
|
||||||
|
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
// TODO: other forms of media subtitle
|
||||||
if self.itemType == .episode {
|
if self.itemType == .episode {
|
||||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||||
|
@ -101,8 +106,8 @@ extension BaseItemDto {
|
||||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(item: self,
|
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
|
||||||
title: self.name ?? "",
|
title: modifiedSelfItem.name ?? "",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
streamURL: streamURL.url!,
|
streamURL: streamURL.url!,
|
||||||
hlsURL: hlsURL.url!,
|
hlsURL: hlsURL.url!,
|
||||||
|
|
|
@ -156,9 +156,9 @@ public extension BaseItemDto {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemProgressString() -> String {
|
func getItemProgressString() -> String? {
|
||||||
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
|
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
|
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
|
||||||
|
@ -208,4 +208,60 @@ public extension BaseItemDto {
|
||||||
return getPrimaryImage(maxWidth: maxWidth)
|
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
|
// MARK: PortraitImageStackable
|
||||||
extension BaseItemPerson: PortraitImageStackable {
|
extension BaseItemPerson: PortraitImageStackable {
|
||||||
|
public var portraitImageID: String {
|
||||||
|
return (id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials
|
||||||
|
}
|
||||||
|
|
||||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||||
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
|
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
|
||||||
}
|
}
|
||||||
|
@ -68,7 +72,7 @@ extension BaseItemPerson: PortraitImageStackable {
|
||||||
return self.name ?? ""
|
return self.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
public var description: String? {
|
public var subtitle: String? {
|
||||||
return self.firstRole()
|
return self.firstRole()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +85,10 @@ extension BaseItemPerson: PortraitImageStackable {
|
||||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||||
return String(initials)
|
return String(initials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var showTitle: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: DiplayedType
|
// MARK: DiplayedType
|
||||||
|
|
|
@ -12,7 +12,9 @@ import Foundation
|
||||||
public protocol PortraitImageStackable {
|
public protocol PortraitImageStackable {
|
||||||
func imageURLContsructor(maxWidth: Int) -> URL
|
func imageURLContsructor(maxWidth: Int) -> URL
|
||||||
var title: String { get }
|
var title: String { get }
|
||||||
var description: String? { get }
|
var subtitle: String? { get }
|
||||||
var blurHash: String { get }
|
var blurHash: String { get }
|
||||||
var failureInitials: 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 processDeepLink = Notification.Name("processDeepLink")
|
||||||
static let didPurge = Notification.Name("didPurge")
|
static let didPurge = Notification.Name("didPurge")
|
||||||
static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI")
|
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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", 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 overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, 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)
|
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
|
@ -15,12 +15,12 @@ import Stinsen
|
||||||
final class EpisodeItemViewModel: ItemViewModel {
|
final class EpisodeItemViewModel: ItemViewModel {
|
||||||
|
|
||||||
@RouterObject var itemRouter: ItemCoordinator.Router?
|
@RouterObject var itemRouter: ItemCoordinator.Router?
|
||||||
var seasonEpisodes: [BaseItemDto] = []
|
@Published var series: BaseItemDto?
|
||||||
|
|
||||||
override init(item: BaseItemDto) {
|
override init(item: BaseItemDto) {
|
||||||
super.init(item: item)
|
super.init(item: item)
|
||||||
|
|
||||||
getSeasonEpisodes()
|
getEpisodeSeries()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func getItemDisplayName() -> String {
|
override func getItemDisplayName() -> String {
|
||||||
|
@ -32,41 +32,15 @@ final class EpisodeItemViewModel: ItemViewModel {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeToSeasonItem() {
|
func getEpisodeSeries() {
|
||||||
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() {
|
|
||||||
guard let id = item.seriesId else { return }
|
guard let id = item.seriesId else { return }
|
||||||
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
self?.handleAPIRequestError(completion: completion)
|
self?.handleAPIRequestError(completion: completion)
|
||||||
}, receiveValue: { [weak self] item in
|
}, receiveValue: { [weak self] item in
|
||||||
self?.itemRouter?.route(to: \.item, item)
|
self?.series = item
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.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
|
let nc = SwiftfinNotificationCenter.main
|
||||||
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
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(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(didEndPlayback), name: SwiftfinNotificationCenter.Keys.didEndPlayback, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didSignIn() {
|
deinit {
|
||||||
|
SwiftfinNotificationCenter.main.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func didSignIn() {
|
||||||
for cancellable in cancellables {
|
for cancellable in cancellables {
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
@ -47,7 +52,7 @@ final class HomeViewModel: ViewModel {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didSignOut() {
|
@objc private func didSignOut() {
|
||||||
for cancellable in cancellables {
|
for cancellable in cancellables {
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
@ -55,8 +60,21 @@ final class HomeViewModel: ViewModel {
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh() {
|
@objc private func didEndPlayback() {
|
||||||
|
refreshResumeItems()
|
||||||
|
refreshNextUpItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func refresh() {
|
||||||
LogManager.shared.log.debug("Refresh called.")
|
LogManager.shared.log.debug("Refresh called.")
|
||||||
|
|
||||||
|
refreshLibrariesLatest()
|
||||||
|
refreshResumeItems()
|
||||||
|
refreshNextUpItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Libraries Latest Items
|
||||||
|
private func refreshLibrariesLatest() {
|
||||||
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
|
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
|
@ -101,7 +119,10 @@ final class HomeViewModel: ViewModel {
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Resume Items
|
||||||
|
private func refreshResumeItems() {
|
||||||
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
|
||||||
mediaTypes: ["Video"],
|
mediaTypes: ["Video"],
|
||||||
|
@ -121,7 +142,10 @@ final class HomeViewModel: ViewModel {
|
||||||
self.resumeItems = response.items ?? []
|
self.resumeItems = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Next Up Items
|
||||||
|
private func refreshNextUpItems() {
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters])
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters])
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
|
|
@ -19,6 +19,8 @@ class ItemViewModel: ViewModel {
|
||||||
@Published var similarItems: [BaseItemDto] = []
|
@Published var similarItems: [BaseItemDto] = []
|
||||||
@Published var isWatched = false
|
@Published var isWatched = false
|
||||||
@Published var isFavorited = false
|
@Published var isFavorited = false
|
||||||
|
@Published var informationItems: [BaseItemDto.ItemDetail]
|
||||||
|
@Published var mediaItems: [BaseItemDto.ItemDetail]
|
||||||
var itemVideoPlayerViewModel: VideoPlayerViewModel?
|
var itemVideoPlayerViewModel: VideoPlayerViewModel?
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
|
@ -30,6 +32,9 @@ class ItemViewModel: ViewModel {
|
||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
informationItems = item.createInformationItems()
|
||||||
|
mediaItems = item.createMediaItems()
|
||||||
|
|
||||||
isFavorited = item.userData?.isFavorite ?? false
|
isFavorited = item.userData?.isFavorite ?? false
|
||||||
isWatched = item.userData?.played ?? false
|
isWatched = item.userData?.played ?? false
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -41,12 +46,17 @@ class ItemViewModel: ViewModel {
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
} receiveValue: { videoPlayerViewModel in
|
} receiveValue: { videoPlayerViewModel in
|
||||||
self.itemVideoPlayerViewModel = videoPlayerViewModel
|
self.itemVideoPlayerViewModel = videoPlayerViewModel
|
||||||
|
self.mediaItems = videoPlayerViewModel.item.createMediaItems()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playButtonText() -> String {
|
func playButtonText() -> String {
|
||||||
return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString()
|
if let itemProgressString = item.getItemProgressString() {
|
||||||
|
return itemProgressString
|
||||||
|
}
|
||||||
|
|
||||||
|
return L10n.play
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemDisplayName() -> String {
|
func getItemDisplayName() -> String {
|
||||||
|
|
Loading…
Reference in New Issue