Merge pull request #72 from PangMo5/PangMo5/refactoring-2

Structural improvements - 2
This commit is contained in:
aiden vigue 2021-06-18 21:24:29 -04:00 committed by GitHub
commit a4054060e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 934 additions and 661 deletions

View File

@ -146,6 +146,24 @@
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; };
62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; };
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */; };
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */; };
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */; };
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */; };
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */; };
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */; };
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; };
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; };
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; };
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; };
62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; };
62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; };
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; };
62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
@ -338,6 +356,15 @@
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemViewModel.swift; sourceTree = "<group>"; };
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemViewModel.swift; sourceTree = "<group>"; };
62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemViewModel.swift; sourceTree = "<group>"; };
62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemViewModel.swift; sourceTree = "<group>"; };
62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterViewModel.swift; sourceTree = "<group>"; };
62E632F2267D54030063E547 /* DetailItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItemViewModel.swift; sourceTree = "<group>"; };
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
@ -402,6 +429,15 @@
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
625CB57B2678CE1000530A6E /* ViewModel.swift */,
536D3D75267BA9BB0004248C /* MainTabViewModel.swift */,
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */,
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */,
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */,
62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */,
62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */,
62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */,
62E632F2267D54030063E547 /* DetailItemViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -440,6 +476,7 @@
535870752669D60C00D05A09 /* Shared */ = {
isa = PBXGroup;
children = (
62E632F1267D53B00063E547 /* Protocols */,
62EC352A26766657000E9F2D /* Singleton */,
532175392671BCED005491E6 /* ViewModels */,
621338912660106C00A81A2A /* Extensions */,
@ -683,6 +720,13 @@
path = WidgetExtension;
sourceTree = "<group>";
};
62E632F1267D53B00063E547 /* Protocols */ = {
isa = PBXGroup;
children = (
);
path = Protocols;
sourceTree = "<group>";
};
62EC352A26766657000E9F2D /* Singleton */ = {
isa = PBXGroup;
children = (
@ -882,21 +926,30 @@
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
53ABFDDE267974E300886593 /* SplashView.swift in Sources */,
53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */,
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */,
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */,
62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */,
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */,
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */,
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
@ -921,8 +974,10 @@
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
5362E507267D4707000E2F71 /* HeartbeatChannel.swift in Sources */,
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
5362E506267D4707000E2F71 /* MediaControlChannel.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
5362E500267D4707000E2F71 /* CastClient.swift in Sources */,
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
@ -937,11 +992,13 @@
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
5362E50E267D4707000E2F71 /* CastStatus.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
53892770263C25230035E14B /* NextUpView.swift in Sources */,
5362E513267D4707000E2F71 /* CastMultizoneDevice.swift in Sources */,
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
5362E50A267D4707000E2F71 /* RequestSink.swift in Sources */,
@ -951,7 +1008,9 @@
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
5362E503267D4707000E2F71 /* Channelable.swift in Sources */,
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */,
5362E50C267D4707000E2F71 /* DeviceDiscoveryChannel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
5362E50D267D4707000E2F71 /* CastDevice.swift in Sources */,
@ -959,7 +1018,10 @@
5362E509267D4707000E2F71 /* DeviceConnectionChannel.swift in Sources */,
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
5362E512267D4707000E2F71 /* CastMedia.swift in Sources */,
5362E519267D4707000E2F71 /* cast_channel.pb.swift in Sources */,
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,

View File

@ -29,7 +29,7 @@
}
},
{
"package": "jellyfin-sdk-swift",
"package": "JellyfinAPI",
"repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift",
"state": {
"branch": "main",
@ -38,7 +38,7 @@
}
},
{
"package": "keychain-swift",
"package": "KeychainSwift",
"repositoryURL": "https://github.com/evgenyneu/keychain-swift",
"state": {
"branch": null,
@ -83,7 +83,7 @@
}
},
{
"package": "swift-protobuf",
"package": "SwiftProtobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
@ -92,7 +92,7 @@
}
},
{
"package": "SwiftUI-Introspect",
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
"state": {
"branch": null,

View File

@ -11,62 +11,14 @@ import Combine
struct EpisodeItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: EpisodeItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
@EnvironmentObject private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@State private var settingState: Bool = true
@State private var watched: Bool = false {
didSet {
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
@State
private var favorite: Bool = false {
didSet {
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -74,27 +26,27 @@ struct EpisodeItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
Spacer()
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(y: 5)
HStack {
Text(String(item.productionYear ?? 0)).font(.subheadline)
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -109,11 +61,11 @@ struct EpisodeItemView: View {
HStack {
// Play button
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -125,19 +77,21 @@ struct EpisodeItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
} else {
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -145,6 +99,7 @@ struct EpisodeItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.top, 8)
}
@ -160,21 +115,21 @@ struct EpisodeItemView: View {
Spacer()
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(withGenre: genre)
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
Text(genre.name ?? "").font(.footnote)
}
@ -182,16 +137,16 @@ struct EpisodeItemView: View {
}.padding(.leading, 16).padding(.trailing, 16)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
}) {
VStack {
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
@ -213,13 +168,13 @@ struct EpisodeItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -233,7 +188,7 @@ struct EpisodeItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(maxWidth: 200), bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -241,16 +196,16 @@ struct EpisodeItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -265,23 +220,23 @@ struct EpisodeItemView: View {
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(x: 14, y: 0)
Spacer().frame(height: 1)
HStack {
Text(String(item.productionYear ?? 0)).font(.subheadline)
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -289,10 +244,10 @@ struct EpisodeItemView: View {
.overlay(RoundedRectangle(cornerRadius: 2)
.stroke(Color.secondary, lineWidth: 1))
}
if item.communityRating != nil {
if viewModel.item.communityRating != nil {
HStack {
Image(systemName: "star").foregroundColor(.secondary)
Text(String(item.communityRating!)).font(.subheadline)
Text(String(viewModel.item.communityRating!)).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -306,20 +261,21 @@ struct EpisodeItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary)
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -327,23 +283,24 @@ struct EpisodeItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 3)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(withGenre: genre)
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
Text(genre.name ?? "").font(.footnote)
}
@ -353,16 +310,16 @@ struct EpisodeItemView: View {
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
}) {
VStack {
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
@ -384,13 +341,13 @@ struct EpisodeItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -409,15 +366,10 @@ struct EpisodeItemView: View {
}
}
}
.onAppear(perform: {
favorite = item.userData?.isFavorite ?? false
watched = item.userData?.played ?? false
settingState = false
})
.onRotate(perform: { orientation in
self.orientation = orientation
})
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.seriesName ?? "") - S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.navigationTitle("\(viewModel.item.seriesName ?? "") - S\(String(viewModel.item.parentIndexNumber ?? 0)):E\(String(viewModel.item.indexNumber ?? 0))")
}
}

View File

@ -44,8 +44,7 @@ struct HomeView: View {
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
Spacer()
NavigationLink(destination: LazyView {
LibraryView(usingParentID: libraryID,
title: library?.name ?? "", usingFilters: viewModel.recentFilterSet)
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
}) {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
@ -53,7 +52,7 @@ struct HomeView: View {
}
}
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
LatestMediaView(usingParentID: libraryID)
LatestMediaView(viewModel: .init(libraryID: libraryID))
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
}

View File

@ -39,13 +39,13 @@ struct ItemView: View {
}
VStack {
if item.type == "Movie" {
MovieItemView(item: item)
MovieItemView(viewModel: .init(item: item))
} else if item.type == "Season" {
SeasonItemView(item: item)
SeasonItemView(viewModel: .init(item: item))
} else if item.type == "Series" {
SeriesItemView(item: item)
SeriesItemView(viewModel: .init(item: item))
} else if item.type == "Episode" {
EpisodeItemView(item: item)
EpisodeItemView(viewModel: .init(item: item))
} else {
Text("Type: \(item.type ?? "") not implemented yet :(")
}

View File

@ -5,44 +5,20 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
import Combine
import JellyfinAPI
import SwiftUI
struct LatestMediaView: View {
@StateObject
var tempViewModel = ViewModel()
@State var items: [BaseItemDto] = []
private var library_id: String = ""
@State private var viewDidLoad: Bool = false
init(usingParentID: String) {
library_id = usingParentID
}
func onAppear() {
if viewDidLoad == true {
return
}
viewDidLoad = true
DispatchQueue.global(qos: .userInitiated).async {
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
items = response
})
.store(in: &tempViewModel.cancellables)
}
}
var viewModel: LatestMediaViewModel
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollView(.horizontal, showsIndicators: false) {
ZStack {
LazyHStack {
Spacer().frame(width: 16)
ForEach(items, id: \.id) { item in
ForEach(viewModel.items, id: \.id) { item in
if item.type == "Series" || item.type == "Movie" {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
@ -69,10 +45,13 @@ struct LatestMediaView: View {
}
}
}
if viewModel.isLoading {
ProgressView()
}
}
.frame(height: 190)
}
.onAppear(perform: onAppear)
.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
.frame(height: 190)
}
.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
}
}

View File

@ -5,75 +5,80 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
import SwiftUI
struct LibraryFilterView: View {
@Binding var filter: LibraryFilters
@Environment(\.presentationMode)
var presentationMode
@Binding
var filters: LibraryFilters
@StateObject
var viewModel: LibraryFilterViewModel
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType]) {
_filters = filters
_viewModel = StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType))
}
var body: some View {
EmptyView()
/*
NavigationView {
LoadingView(isShowing: $isLoading) {
ZStack {
Form {
Toggle("Only show unplayed items", isOn: $onlyUnplayed)
.onChange(of: onlyUnplayed) { value in
if value {
filter.filterTypes.append(.isUnplayed)
} else {
filter.filterTypes.removeAll { $0 == .isUnplayed }
}
}
MultiSelector(label: "Genres",
options: allGenres,
optionToString: { $0.name },
selected: $selectedGenres)
.onChange(of: selectedGenres) { genres in
filter.genres = genres.map(\.id)
}
MultiSelector(label: "Parental Ratings",
options: allRatings,
optionToString: { $0.name },
selected: $selectedRatings)
.onChange(of: selectedRatings) { ratings in
filter.officialRatings = ratings.map(\.id)
}
Section(header: Text("Sort settings")) {
Picker("Sort by", selection: $sortBySelection) {
Text("Name").tag("SortName")
Text("Date Added").tag("DateCreated")
Text("Date Played").tag("DatePlayed")
Text("Date Released").tag("PremiereDate")
Text("Runtime").tag("Runtime")
}.onChange(of: sortBySelection) { value in
guard let sort = SortType(rawValue: value) else { return }
filter.sort = sort
}
Picker("Sort order", selection: $sortOrder) {
Text("Ascending").tag("Ascending")
Text("Descending").tag("Descending")
}.onChange(of: sortOrder) { order in
guard let asc = ASC(rawValue: order) else { return }
filter.asc = asc
}
if viewModel.enabledFilterType.contains(.genre) {
MultiSelector(label: "Genres",
options: viewModel.possibleGenres,
optionToString: { $0.name ?? "" },
selected: $viewModel.modifyedFilters.withGenres)
}
if viewModel.enabledFilterType.contains(.filter) {
MultiSelector(label: "Filters",
options: viewModel.possibleItemFilters,
optionToString: { $0.localized },
selected: $viewModel.modifyedFilters.filters)
}
if viewModel.enabledFilterType.contains(.tag) {
MultiSelector(label: "Tags",
options: viewModel.possibleTags,
optionToString: { $0 },
selected: $viewModel.modifyedFilters.tags)
}
if viewModel.enabledFilterType.contains(.sortBy) {
MultiSelector(label: "Sort by",
options: viewModel.possibleSortBys,
optionToString: { $0.localized },
selected: $viewModel.modifyedFilters.sortBy)
}
if viewModel.enabledFilterType.contains(.sortOrder) {
MultiSelector(label: "Sort Order",
options: viewModel.possibleSortOrders,
optionToString: { $0.localized },
selected: $viewModel.modifyedFilters.sortOrder)
}
}
}.onAppear(perform: onAppear)
.navigationBarTitle("Filters", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
HStack {
Text("Back").font(.callout)
}
}
if viewModel.isLoading {
ProgressView()
}
}
.navigationBarTitle("Filters", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
}
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
self.filters = viewModel.modifyedFilters
presentationMode.wrappedValue.dismiss()
} label: {
Text("Apply")
}
}
}
}
*/
}
}

View File

@ -17,7 +17,7 @@ struct LibraryListView: View {
switch library.id {
case "favorites":
NavigationLink(destination: LazyView {
LibraryView(usingParentID: "", title: library.name ?? "", usingFilters: viewModel.withFavorites)
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: library.name ?? "")
}) {
Text(library.name ?? "")
}
@ -29,7 +29,7 @@ struct LibraryListView: View {
}
default:
NavigationLink(destination: LazyView {
LibraryView(usingParentID: library.id ?? "", title: library.name ?? "")
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
}) {
Text(library.name ?? "")
}
@ -39,7 +39,7 @@ struct LibraryListView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
NavigationLink(destination: LazyView {
LibrarySearchView(usingParentID: "")
LibrarySearchView(viewModel: .init(parentID: nil))
}) {
Image(systemName: "magnifyingglass")
}

View File

@ -5,67 +5,33 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
import Combine
import JellyfinAPI
import SwiftUI
struct LibrarySearchView: View {
@StateObject
var tempViewModel = ViewModel()
@State private var items: [BaseItemDto] = []
@State private var searchQuery: String = ""
@State private var isLoading: Bool = false
private var usingParentID: String = ""
@State private var lastSearchTime: Double = CACurrentMediaTime()
init(usingParentID: String) {
self.usingParentID = usingParentID
}
func onAppear() {
recalcTracks()
requestSearch(query: "")
}
func requestSearch(query: String) {
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
items = response.items ?? []
isLoading = false
})
.store(in: &tempViewModel.cancellables)
}
}
var viewModel: LibrarySearchViewModel
// MARK: tracks for grid
@State private var tracks: [GridItem] = []
@State
private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
func recalcTracks() {
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
tracks = []
for _ in 0 ..< trkCnt {
tracks.append(GridItem(.flexible()))
}
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
}
var body: some View {
VStack {
Spacer().frame(height: 6)
SearchBar(text: $searchQuery)
if isLoading == true {
Spacer()
ProgressView()
Spacer()
} else {
if !items.isEmpty {
ScrollView(.vertical) {
SearchBar(text: $viewModel.searchQuery)
ZStack {
ScrollView(.vertical) {
if !viewModel.items.isEmpty {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
ForEach(items, id: \.id) { item in
ForEach(viewModel.items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
@ -87,26 +53,20 @@ struct LibrarySearchView: View {
}.frame(width: 100)
}
}
Spacer().frame(height: 16)
}
Spacer().frame(height: 16)
.onRotate { _ in
.onRotate { _ in
recalcTracks()
}
} else if !viewModel.isLoading {
Text("No results :(")
}
} else {
Text("No results :(")
}
if viewModel.isLoading {
ProgressView()
}
}
}
.onAppear(perform: onAppear)
.navigationBarTitle("Search", displayMode: .inline)
.onChange(of: searchQuery) { query in
if CACurrentMediaTime() - lastSearchTime > 0.5 {
lastSearchTime = CACurrentMediaTime()
requestSearch(query: query)
}
}
}
}
// stream NM5 by nicki!

View File

@ -6,190 +6,132 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import NukeUI
import JellyfinAPI
import Combine
import JellyfinAPI
import NukeUI
import SwiftUI
struct LibraryView: View {
@StateObject
var tempViewModel = ViewModel()
@State private var items: [BaseItemDto] = []
@State private var isLoading: Bool = false
private var usingParentID: String = ""
private var title: String = ""
private var filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: ["SortName"])
private var personId: String = ""
private var genre: String = ""
private var studio: String = ""
@State private var totalPages: Int = 0
@State private var currentPage: Int = 0
@State private var isSearching: String? = ""
@State private var viewDidLoad: Bool = false
init(usingParentID: String, title: String) {
self.usingParentID = usingParentID
self.title = title
}
init(usingParentID: String, title: String, usingFilters: LibraryFilters) {
self.usingParentID = usingParentID
self.title = title
self.filters = usingFilters
}
init(withPerson: BaseItemPerson) {
self.usingParentID = ""
self.title = withPerson.name ?? ""
self.personId = withPerson.id!
}
init(withGenre: NameGuidPair) {
self.usingParentID = ""
self.title = withGenre.name ?? ""
self.genre = withGenre.id!
}
init(withStudio: NameGuidPair) {
self.usingParentID = ""
self.title = withStudio.name ?? ""
self.studio = withStudio.id!
}
func onAppear() {
recalcTracks()
if viewDidLoad {
return
}
isLoading = true
items = []
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
.sink(receiveCompletion: { completion in
print(completion)
isLoading = false
}, receiveValue: { response in
let x = ceil(Double(response.totalRecordCount!) / 100.0)
totalPages = Int(x)
items = response.items ?? []
isLoading = false
viewDidLoad = true
})
.store(in: &tempViewModel.cancellables)
}
}
var viewModel: LibraryViewModel
var title: String
// MARK: tracks for grid
@State private var tracks: [GridItem] = []
@State
var isShowingSearchView = false
@State
var isShowingFilterView = false
@State
private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
func recalcTracks() {
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
_tracks.wrappedValue = []
for _ in 0 ..< trkCnt {
_tracks.wrappedValue.append(GridItem(.flexible()))
}
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
}
var body: some View {
ZStack {
if isLoading == true {
Group {
if viewModel.isLoading == true {
ProgressView()
} else {
if !items.isEmpty {
VStack {
ScrollView(.vertical) {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if item.productionYear != nil {
Text(String(item.productionYear!))
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else {
Text(item.type ?? "")
}
}.frame(width: 100)
}
}
}.onRotate { _ in
recalcTracks()
}
if totalPages > 1 {
HStack {
Spacer()
HStack {
Button {
currentPage = currentPage - 1
onAppear()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 25))
}.disabled(currentPage == 0)
Text("Page \(String(currentPage+1)) of \(String(totalPages))")
.font(.headline)
} else if !viewModel.items.isEmpty {
VStack {
ScrollView(.vertical) {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
ForEach(viewModel.items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
Button {
currentPage = currentPage + 1
onAppear()
} label: {
Image(systemName: "chevron.right")
.font(.system(size: 25))
}.disabled(currentPage > totalPages - 1)
}
Spacer()
.foregroundColor(.primary)
.lineLimit(1)
if item.productionYear != nil {
Text(String(item.productionYear!))
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else {
Text(item.type ?? "")
}
}.frame(width: 100)
}
}
Spacer().frame(height: 16)
}.onRotate { _ in
recalcTracks()
}
if viewModel.hasNextPage || viewModel.hasPreviousPage {
HStack {
Spacer()
HStack {
Button {
viewModel.requestPreviousPage()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 25))
}.disabled(viewModel.hasPreviousPage)
Text("Page \(String(viewModel.currentPage + 1)) of \(String(viewModel.totalPages))")
.font(.headline)
.fontWeight(.semibold)
Button {
viewModel.requestNextPage()
} label: {
Image(systemName: "chevron.right")
.font(.system(size: 25))
}.disabled(viewModel.hasNextPage)
}
Spacer()
}
}
Spacer().frame(height: 16)
}
} else {
Text("No results.")
}
} else {
Text("No results.")
}
}
.onAppear(perform: onAppear)
.navigationBarTitle(title, displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if currentPage > 0 {
if viewModel.hasPreviousPage {
Button {
currentPage = currentPage - 1
onAppear()
viewModel.requestPreviousPage()
} label: {
Image(systemName: "chevron.left")
}
}
if currentPage < totalPages - 1 {
if viewModel.hasNextPage {
Button {
currentPage = currentPage + 1
onAppear()
viewModel.requestNextPage()
} label: {
Image(systemName: "chevron.right")
}
}
if usingParentID != "" {
NavigationLink(destination: LibrarySearchView(usingParentID: usingParentID)) {
Image(systemName: "magnifyingglass")
}
Button(action: {
isShowingFilterView = true
}) {
Image(systemName: "line.horizontal.3.decrease.circle")
}
Button(action: {
isShowingSearchView = true
}) {
Image(systemName: "magnifyingglass")
}
}
}
.sheet(isPresented: $isShowingFilterView) {
LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType)
}
.background(
NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)),
isActive: $isShowingSearchView) {
EmptyView()
}
)
}
}

View File

@ -11,7 +11,7 @@ import SwiftUI
struct MovieItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: MovieItemViewModel
@State
private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass)
@ -21,60 +21,9 @@ struct MovieItemView: View {
@EnvironmentObject
private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@State
private var settingState: Bool = true
@State
private var watched: Bool = false {
didSet {
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
@State
private var favorite: Bool = false {
didSet {
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item
.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -82,29 +31,29 @@ struct MovieItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(maxWidth: 120))
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120))
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
Spacer()
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.offset(y: 5)
HStack {
if item.productionYear != nil {
Text(String(item.productionYear ?? 0)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -119,11 +68,11 @@ struct MovieItemView: View {
HStack {
// Play button
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -135,19 +84,21 @@ struct MovieItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
} else {
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -155,6 +106,7 @@ struct MovieItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.top, 8)
}
@ -173,21 +125,21 @@ struct MovieItemView: View {
Spacer()
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(withGenre: genre)
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
Text(genre.name ?? "").font(.footnote)
}
@ -195,16 +147,16 @@ struct MovieItemView: View {
}.padding(.leading, 16).padding(.trailing, 16)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
}) {
VStack {
ImageView(src: person
@ -228,13 +180,14 @@ struct MovieItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -248,8 +201,8 @@ struct MovieItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(maxWidth: 200),
bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -257,17 +210,17 @@ struct MovieItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getPrimaryImage(maxWidth: 120),
bh: item.getPrimaryImageBlurHash())
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120),
bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -282,25 +235,25 @@ struct MovieItemView: View {
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(x: 14, y: 0)
Spacer().frame(height: 1)
HStack {
if item.productionYear != nil {
Text(String(item.productionYear ?? 0)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -308,10 +261,10 @@ struct MovieItemView: View {
.overlay(RoundedRectangle(cornerRadius: 2)
.stroke(Color.secondary, lineWidth: 1))
}
if item.communityRating != nil {
if viewModel.item.communityRating != nil {
HStack {
Image(systemName: "star").foregroundColor(.secondary)
Text(String(item.communityRating!)).font(.subheadline)
Text(String(viewModel.item.communityRating!)).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -325,20 +278,21 @@ struct MovieItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary)
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -346,23 +300,24 @@ struct MovieItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 3)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(withGenre: genre)
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
Text(genre.name ?? "").font(.footnote)
}
@ -372,16 +327,16 @@ struct MovieItemView: View {
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
}) {
VStack {
ImageView(src: person
@ -407,13 +362,13 @@ struct MovieItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -432,15 +387,10 @@ struct MovieItemView: View {
}
}
}
.onAppear(perform: {
favorite = item.userData?.isFavorite ?? false
watched = item.userData?.played ?? false
settingState = false
})
.onRotate {
orientation = $0
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name ?? "")
.navigationTitle(viewModel.item.name ?? "")
}
}

View File

@ -19,7 +19,7 @@ struct SearchBar: View {
TextField("Search ...", text: $text)
.padding(7)
.padding(.horizontal, 25)
.padding(.horizontal, 16)
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal, 10)

View File

@ -11,45 +11,17 @@ import JellyfinAPI
struct SeasonItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: SeasonItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
var item: BaseItemDto = BaseItemDto()
@State private var episodes: [BaseItemDto] = []
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
init(item: BaseItemDto) {
self.item = item
}
func onAppear() {
if viewDidLoad {
return
}
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
.sink(receiveCompletion: { completion in
print(completion)
isLoading = false
}, receiveValue: { response in
viewDidLoad = true
episodes = response.items ?? []
})
.store(in: &tempViewModel.cancellables)
}
}
@ViewBuilder
var portraitHeaderView: some View {
if isLoading {
if viewModel.isLoading {
EmptyView()
} else {
ImageView(src: item.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -57,17 +29,17 @@ struct SeasonItemView: View {
var portraitHeaderOverlayView: some View {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(y: -4)
if item.productionYear != nil {
Text(String(item.productionYear!)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear!)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
@ -85,15 +57,15 @@ struct SeasonItemView: View {
overlayAlignment: .bottomLeading,
headerHeight: UIScreen.main.bounds.width * 0.5625) {
LazyVStack(alignment: .leading) {
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
ForEach(episodes, id: \.id) { episode in
ForEach(viewModel.episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
@ -133,13 +105,13 @@ struct SeasonItemView: View {
}.offset(x: 12, y: 0)
}
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -155,7 +127,7 @@ struct SeasonItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), bh: viewModel.item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -164,12 +136,12 @@ struct SeasonItemView: View {
HStack {
VStack(alignment: .leading) {
Spacer().frame(height: 16)
ImageView(src: item.getPrimaryImage(maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 4)
if item.productionYear != nil {
Text(String(item.productionYear!)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear!)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
}
@ -178,15 +150,15 @@ struct SeasonItemView: View {
ScrollView {
Spacer().frame(height: 16)
LazyVStack(alignment: .leading) {
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
ForEach(episodes, id: \.id) { episode in
ForEach(viewModel.episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
@ -226,13 +198,13 @@ struct SeasonItemView: View {
}.offset(x: 12, y: 0)
}
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(withStudio: studio)
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
Text(studio.name ?? "").font(.footnote)
}
@ -250,16 +222,15 @@ struct SeasonItemView: View {
}
var body: some View {
if isLoading {
if viewModel.isLoading {
ProgressView()
.onAppear(perform: onAppear)
} else {
innerBody
.onRotate {
orientation = $0
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.name ?? "") - \(item.seriesName ?? "")")
.navigationTitle("\(viewModel.item.name ?? "") - \(viewModel.item.seriesName ?? "")")
}
}
}

View File

@ -11,54 +11,23 @@ import Combine
struct SeriesItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: SeriesItemViewModel
var item: BaseItemDto
@State
private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
@State private var seasons: [BaseItemDto] = []
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
func onAppear() {
recalcTracks()
if viewDidLoad {
return
}
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
isLoading = false
viewDidLoad = true
seasons = response.items ?? []
})
.store(in: &tempViewModel.cancellables)
}
}
// MARK: Grid tracks
func recalcTracks() {
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125))
tracks = []
for _ in (0..<trkCnt) {
tracks.append(GridItem.init(.flexible()))
}
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
}
@State private var tracks: [GridItem] = []
var body: some View {
if isLoading {
if viewModel.isLoading {
ProgressView()
.onAppear(perform: onAppear)
} else {
ScrollView(.vertical) {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
ForEach(seasons, id: \.id) { season in
ForEach(viewModel.seasons, id: \.id) { season in
NavigationLink(destination: ItemView(item: season)) {
VStack(alignment: .leading) {
ImageView(src: season.getPrimaryImage(maxWidth: 100), bh: season.getPrimaryImageBlurHash())
@ -85,7 +54,7 @@ struct SeriesItemView: View {
}
}
.overrideViewPreference(.unspecified)
.navigationTitle(item.name ?? "")
.navigationTitle(viewModel.item.name ?? "")
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -7,44 +7,44 @@
import SwiftUI
private struct MultiSelectionView<Selectable: Identifiable & Hashable>: View {
private struct MultiSelectionView<Selectable: Hashable>: View {
let options: [Selectable]
let optionToString: (Selectable) -> String
let label: String
@Binding var selected: Set<Selectable>
@Binding var selected: Array<Selectable>
var body: some View {
List {
ForEach(options) { selectable in
ForEach(options, id: \.self) { selectable in
Button(action: { toggleSelection(selectable: selectable) }) {
HStack {
Text(optionToString(selectable)).foregroundColor(Color.primary)
Spacer()
if selected.contains { $0.id == selectable.id } {
if selected.contains { $0 == selectable } {
Image(systemName: "checkmark").foregroundColor(.accentColor)
}
}
}.tag(selectable.id)
}.tag(selectable)
}
}.listStyle(GroupedListStyle())
}
private func toggleSelection(selectable: Selectable) {
if let existingIndex = selected.firstIndex(where: { $0.id == selectable.id }) {
if let existingIndex = selected.firstIndex(where: { $0 == selectable }) {
selected.remove(at: existingIndex)
} else {
selected.insert(selectable)
selected.append(selectable)
}
}
}
struct MultiSelector<Selectable: Identifiable & Hashable>: View {
struct MultiSelector<Selectable: Hashable>: View {
let label: String
let options: [Selectable]
let optionToString: (Selectable) -> String
var selected: Binding<Set<Selectable>>
var selected: Binding<Array<Selectable>>
private var formattedSelectedListString: String {
ListFormatter.localizedString(byJoining: selected.wrappedValue.map { optionToString($0) })

View File

@ -5,15 +5,16 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import Combine
import Foundation
import JellyfinAPI
struct LibraryFilters: Codable, Hashable {
var filters: [ItemFilter] = []
var sortOrder: [APISortOrder] = [.descending]
var withGenres: [NameGuidPair] = []
var sortBy: [String] = ["SortName"]
var tags: [String] = []
var sortBy: [SortBy] = [.name]
}
public enum SortBy: String, Codable, CaseIterable {
@ -22,3 +23,52 @@ public enum SortBy: String, Codable, CaseIterable {
case name = "SortName"
case dateAdded = "DateCreated"
}
extension SortBy {
var localized: String {
switch self {
case .productionYear:
return "Release Year"
case .premiereDate:
return "Premiere date"
case .name:
return "Title"
case .dateAdded:
return "Date added"
}
}
}
extension ItemFilter {
static var supportedTypes: [ItemFilter] {
[.isUnplayed, isPlayed, .isFavorite, .likes, .isFavoriteOrLikes]
}
var localized: String {
switch self {
case .isUnplayed:
return "Unplayed"
case .isPlayed:
return "Played"
case .isFavorite:
return "Favorites"
case .likes:
return "Liked"
case .isFavoriteOrLikes:
return "Favorites or Liked"
default:
return ""
}
}
}
extension APISortOrder {
var localized: String {
switch self {
case .ascending:
return "Ascending"
case .descending:
return "Descending"
}
}
}

View File

@ -37,7 +37,7 @@ final class ConnectToServerViewModel: ViewModel {
if ServerEnvironment.current.server != nil {
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
self.publicUsers = response
self.isConnectedServer = true
@ -74,7 +74,7 @@ final class ConnectToServerViewModel: ViewModel {
func login() {
SessionManager.current.login(username: username, password: password)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { _ in
})

View File

@ -0,0 +1,73 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import Foundation
import JellyfinAPI
class DetailItemViewModel: ViewModel {
@Published
var item: BaseItemDto
@Published
var isWatched = false
@Published
var isFavorited = false
init(item: BaseItemDto) {
self.item = item
isFavorited = item.userData?.isFavorite ?? false
isWatched = item.userData?.played ?? false
super.init()
}
func updateWatchState() {
if isWatched {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = false
})
.store(in: &cancellables)
} else {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = true
})
.store(in: &cancellables)
}
}
func updateFavoriteState() {
if isFavorited {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = false
})
.store(in: &cancellables)
} else {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = true
})
.store(in: &cancellables)
}
}
}

View File

@ -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 Combine
import Foundation
import JellyfinAPI
final class EpisodeItemViewModel: DetailItemViewModel {
}

View File

@ -24,7 +24,7 @@ final class HomeViewModel: ViewModel {
var nextUpItems = [BaseItemDto]()
// temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
override init() {
super.init()
@ -36,7 +36,7 @@ final class HomeViewModel: ViewModel {
UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
response.items!.forEach { item in
if item.collectionType == "movies" || item.collectionType == "tvshows" {
@ -47,7 +47,7 @@ final class HomeViewModel: ViewModel {
UserAPI.getCurrentUser()
.trackActivity(self.loading)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
self.libraries.forEach { library in
if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! {
@ -64,7 +64,7 @@ final class HomeViewModel: ViewModel {
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
self.resumeItems = response.items ?? []
})
@ -74,7 +74,7 @@ final class HomeViewModel: ViewModel {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
self.nextUpItems = response.items ?? []
})

View File

@ -0,0 +1,46 @@
//
/*
* 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 Combine
import Foundation
import JellyfinAPI
final class LatestMediaViewModel: ViewModel {
@Published
var items = [BaseItemDto]()
var libraryID: String
init(libraryID: String) {
self.libraryID = libraryID
super.init()
requestLatestMedia()
}
func requestLatestMedia() {
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: libraryID,
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
],
enableUserData: true, limit: 12)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] response in
self?.items = response
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,61 @@
//
/*
* 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 Combine
import Foundation
import JellyfinAPI
enum FilterType {
case tag
case genre
case sortOrder
case sortBy
case filter
}
final class LibraryFilterViewModel: ViewModel {
@Published
var modifyedFilters = LibraryFilters()
@Published
var possibleGenres = [NameGuidPair]()
@Published
var possibleTags = [String]()
@Published
var possibleSortOrders = APISortOrder.allCases
@Published
var possibleSortBys = SortBy.allCases
@Published
var possibleItemFilters = ItemFilter.supportedTypes
@Published
var enabledFilterType: [FilterType]
init(filters: LibraryFilters? = nil,
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter]) {
self.enabledFilterType = enabledFilterType
super.init()
if let filters = filters {
self.modifyedFilters = filters
}
requestQueryFilters()
}
func requestQueryFilters() {
FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] queryFilters in
guard let self = self else { return }
self.possibleGenres = queryFilters.genres ?? []
self.possibleTags = queryFilters.tags ?? []
})
.store(in: &cancellables)
}
}

View File

@ -22,14 +22,14 @@ final class LibraryListViewModel: ViewModel {
libraries.append(.init(name: "Favorites", id: "favorites"))
libraries.append(.init(name: "Genres", id: "genres"))
refresh()
requsetLibraries()
}
func refresh() {
func requsetLibraries() {
UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.HandleAPIRequestCompletion(completion: completion)
self.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { response in
self.libraries.append(contentsOf: response.items ?? [])
})

View File

@ -0,0 +1,45 @@
//
/*
* 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 Combine
import Foundation
import JellyfinAPI
final class LibrarySearchViewModel: ViewModel {
@Published
var items = [BaseItemDto]()
@Published
var searchQuery = ""
var parentID: String?
init(parentID: String?) {
self.parentID = parentID
super.init()
$searchQuery
.debounce(for: 0.25, scheduler: DispatchQueue.main)
.sink(receiveValue: search(with:))
.store(in: &cancellables)
}
func search(with query: String) {
ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 60, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] response in
self?.items = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,101 @@
//
/*
* 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 Combine
import Foundation
import JellyfinAPI
final class LibraryViewModel: ViewModel {
var parentID: String?
var person: BaseItemPerson?
var genre: NameGuidPair?
var studio: NameGuidPair?
@Published
var items = [BaseItemDto]()
@Published
var totalPages = 0
@Published
var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
// temp
@Published
var filters: LibraryFilters
var enabledFilterType: [FilterType] {
if genre == nil {
return [.tag, .genre, .sortBy, .sortOrder, .filter]
} else {
return [.tag, .sortBy, .sortOrder, .filter]
}
}
init(parentID: String? = nil,
person: BaseItemPerson? = nil,
genre: NameGuidPair? = nil,
studio: NameGuidPair? = nil,
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]))
{
self.parentID = parentID
self.person = person
self.genre = genre
self.studio = studio
self.filters = filters
super.init()
$filters
.sink(receiveValue: requestItems(with:))
.store(in: &cancellables)
}
func requestItems(with filters: LibraryFilters) {
let personIDs: [String] = [person].compactMap(\.?.id)
let studioIDs: [String] = [studio].compactMap(\.?.id)
let genreIDs: [String]
if filters.withGenres.isEmpty {
genreIDs = [genre].compactMap(\.?.id)
} else {
genreIDs = filters.withGenres.compactMap(\.id)
}
let sortBy = filters.sortBy.map(\.rawValue)
ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true,
searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags,
enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] response in
guard let self = self else { return }
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)
self.totalPages = Int(totalPages)
self.hasPreviousPage = self.currentPage > 0
self.hasNextPage = self.currentPage < self.totalPages - 1
self.items = response.items ?? []
})
.store(in: &cancellables)
}
func requestNextPage() {
currentPage += 1
requestItems(with: filters)
}
func requestPreviousPage() {
currentPage -= 1
requestItems(with: filters)
}
}

View File

@ -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 Combine
import Foundation
import JellyfinAPI
final class MovieItemViewModel: DetailItemViewModel {
}

View File

@ -0,0 +1,40 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import Foundation
import JellyfinAPI
final class SeasonItemViewModel: ViewModel {
@Published
var item: BaseItemDto
@Published
var episodes = [BaseItemDto]()
init(item: BaseItemDto) {
self.item = item
super.init()
requestEpisodes()
}
func requestEpisodes() {
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.user.user_id!,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seasonId: item.id ?? "")
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] response in
self?.episodes = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,38 @@
//
/*
* 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 Combine
import Foundation
import JellyfinAPI
final class SeriesItemViewModel: ViewModel {
@Published
var item: BaseItemDto
@Published
var seasons = [BaseItemDto]()
init(item: BaseItemDto) {
self.item = item
super.init()
requestSeasons()
}
func requestSeasons() {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] response in
self?.seasons = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

@ -32,7 +32,7 @@ class ViewModel: ObservableObject {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
}
func HandleAPIRequestCompletion(completion: Subscribers.Completion<Error>) {
func handleAPIRequestCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break