Merge pull request #72 from PangMo5/PangMo5/refactoring-2
Structural improvements - 2
This commit is contained in:
commit
a4054060e3
|
@ -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 */,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 :(")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?? "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ?? "")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) })
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 ?? []
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 ?? [])
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue