Merge pull request #20 from PangMo5/main
Applying MVVM and Moya on some screens, Changed filter structure
This commit is contained in:
commit
db44b487d4
|
@ -42,6 +42,15 @@
|
|||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; };
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; };
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
|
||||
621338932660107500A81A2A /* String++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String++.swift */; };
|
||||
62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; };
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||
6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; };
|
||||
6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; };
|
||||
6273DD48265F41B3009C1D0B /* JellyfinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */; };
|
||||
6273DD4E265F47B2009C1D0B /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */; };
|
||||
AE8C3154265D60BF008AA076 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE8C3153265D60BF008AA076 /* SettingsModel.swift */; };
|
||||
AE8C3156265D616A008AA076 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE8C3155265D616A008AA076 /* SettingsViewModel.swift */; };
|
||||
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
||||
|
@ -102,6 +111,13 @@
|
|||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
|
||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
||||
6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
621338922660107500A81A2A /* String++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String++.swift"; sourceTree = "<group>"; };
|
||||
62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||
6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPI.swift; sourceTree = "<group>"; };
|
||||
6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||
AE8C3153265D60BF008AA076 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = "<group>"; };
|
||||
AE8C3155265D616A008AA076 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
|
||||
|
@ -114,6 +130,8 @@
|
|||
files = (
|
||||
538CD954263E3DC100BB5AF0 /* SDWebImageSwiftUI in Frameworks */,
|
||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||
6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */,
|
||||
6273DD43265F4195009C1D0B /* Moya in Frameworks */,
|
||||
53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */,
|
||||
5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */,
|
||||
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
||||
|
@ -146,6 +164,10 @@
|
|||
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
621338B12660A06F00A81A2A /* SwiftUI */,
|
||||
621338912660106C00A81A2A /* Extensions */,
|
||||
6273DD4A265F4794009C1D0B /* Domains */,
|
||||
6273DD46265F419B009C1D0B /* APIs */,
|
||||
AE8C3157265D6F5E008AA076 /* Resources */,
|
||||
AE8C3152265D607B008AA076 /* ViewModels */,
|
||||
AE8C3151265D6075008AA076 /* Models */,
|
||||
|
@ -174,6 +196,7 @@
|
|||
53987CA526572F0700E7EA70 /* SeriesItemView.swift */,
|
||||
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */,
|
||||
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||
);
|
||||
path = JellyfinPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -194,6 +217,72 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6213388B265F776B00A81A2A /* Library */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6213388C265F777100A81A2A /* ViewModels */,
|
||||
);
|
||||
path = Library;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6213388C265F777100A81A2A /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6213388D265F777C00A81A2A /* LibraryViewModel.swift */,
|
||||
62133894266096EF00A81A2A /* LibraryListViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
621338912660106C00A81A2A /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
621338922660107500A81A2A /* String++.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
621338B12660A06F00A81A2A /* SwiftUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
621338B22660A07800A81A2A /* LazyView.swift */,
|
||||
);
|
||||
path = SwiftUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6273DD46265F419B009C1D0B /* APIs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */,
|
||||
);
|
||||
path = APIs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6273DD49265F478E009C1D0B /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6273DD4B265F479B009C1D0B /* ViewModels */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6273DD4A265F4794009C1D0B /* Domains */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6213388B265F776B00A81A2A /* Library */,
|
||||
6273DD49265F478E009C1D0B /* Search */,
|
||||
);
|
||||
path = Domains;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6273DD4B265F479B009C1D0B /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AE8C3150265D5FE1008AA076 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -254,6 +343,8 @@
|
|||
538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */,
|
||||
5302F8292658791C00647A2E /* Sentry */,
|
||||
53352570265EA0A0006CCA86 /* Introspect */,
|
||||
6273DD42265F4195009C1D0B /* Moya */,
|
||||
6273DD44265F4195009C1D0B /* CombineMoya */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */;
|
||||
|
@ -292,6 +383,7 @@
|
|||
538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
||||
5302F8282658791C00647A2E /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -321,6 +413,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
621338932660107500A81A2A /* String++.swift in Sources */,
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
|
@ -328,6 +421,7 @@
|
|||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||
53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */,
|
||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||
AE8C3154265D60BF008AA076 /* SettingsModel.swift in Sources */,
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||
|
@ -336,6 +430,10 @@
|
|||
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
||||
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */,
|
||||
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */,
|
||||
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */,
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||
62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */,
|
||||
6273DD48265F41B3009C1D0B /* JellyfinAPI.swift in Sources */,
|
||||
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */,
|
||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
||||
53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */,
|
||||
|
@ -344,6 +442,7 @@
|
|||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
AE8C3156265D616A008AA076 /* SettingsViewModel.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
6273DD4E265F47B2009C1D0B /* LibrarySearchViewModel.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||
|
@ -597,6 +696,14 @@
|
|||
minimumVersion = 2.0.2;
|
||||
};
|
||||
};
|
||||
6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Moya/Moya";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = "15.0.0-alpha.1";
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -630,6 +737,16 @@
|
|||
package = 538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||
productName = SDWebImageSwiftUI;
|
||||
};
|
||||
6273DD42265F4195009C1D0B /* Moya */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */;
|
||||
productName = Moya;
|
||||
};
|
||||
6273DD44265F4195009C1D0B /* CombineMoya */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */;
|
||||
productName = CombineMoya;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
||||
"version": "5.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "async-http-client",
|
||||
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
|
||||
|
@ -37,6 +46,33 @@
|
|||
"version": "1.9.200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Moya",
|
||||
"repositoryURL": "https://github.com/Moya/Moya",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e5a28fb62dd5ff4e17b7025643366550044a40b0",
|
||||
"version": "15.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ReactiveSwift",
|
||||
"repositoryURL": "https://github.com/Moya/ReactiveSwift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f195d82bb30e412e70446e2b4a77e1b514099e88",
|
||||
"version": "6.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "RxSwift",
|
||||
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "254617dd7fae0c45319ba5fbea435bf4d0e15b5d",
|
||||
"version": "5.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SDWebImage",
|
||||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
|
||||
BuildableName = "JellyfinPlayer.app"
|
||||
BlueprintName = "JellyfinPlayer"
|
||||
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
|
||||
BuildableName = "JellyfinPlayer.app"
|
||||
BlueprintName = "JellyfinPlayer"
|
||||
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
|
||||
BuildableName = "JellyfinPlayer.app"
|
||||
BlueprintName = "JellyfinPlayer"
|
||||
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// SearchAPI.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum ImageType: String {
|
||||
case primary = "Primary"
|
||||
case backdrop = "Backdrop"
|
||||
case thumb = "Thumb"
|
||||
case banner = "Banner"
|
||||
}
|
||||
|
||||
enum Field: String {
|
||||
case primaryImageAspectRatio = "PrimaryImageAspectRatio"
|
||||
case basicSyncInfo = "BasicSyncInfo"
|
||||
}
|
||||
|
||||
enum ItemType: String {
|
||||
case movie = "Movie"
|
||||
case series = "Series"
|
||||
}
|
||||
|
||||
enum SortType: String {
|
||||
case name = "SortName"
|
||||
case dateCreated = "DateCreated"
|
||||
case datePlayed = "DatePlayed"
|
||||
case premiereDate = "PremiereDate"
|
||||
case runtime = "Runtime"
|
||||
}
|
||||
|
||||
enum ASC: String {
|
||||
case descending = "Descending"
|
||||
case ascending = "Ascending"
|
||||
}
|
||||
|
||||
enum FilterType: String {
|
||||
case isFavorite = "IsFavorite"
|
||||
case isUnplayed = "IsUnplayed"
|
||||
}
|
||||
|
||||
struct Filter {
|
||||
var imageTypes: [ImageType] = [.primary, .backdrop, .thumb, .banner]
|
||||
var fields: [Field] = [.primaryImageAspectRatio, .basicSyncInfo]
|
||||
var itemTypes: [ItemType] = [.movie, .series]
|
||||
var filterTypes = [FilterType]()
|
||||
var sort: SortType? = .dateCreated
|
||||
var asc: ASC? = .descending
|
||||
var parentID: String?
|
||||
var imageTypeLimit: Int? = 1
|
||||
var recursive = true
|
||||
var genres = [String]()
|
||||
var personIds = [String]()
|
||||
var officialRatings = [String]()
|
||||
}
|
||||
|
||||
extension Filter {
|
||||
var toParamters: [String: Any] {
|
||||
var parameters = [String: Any]()
|
||||
parameters["EnableImageTypes"] = imageTypes.map(\.rawValue).joined(separator: ",")
|
||||
parameters["Fields"] = fields.map(\.rawValue).joined(separator: ",")
|
||||
parameters["Filters"] = filterTypes.map(\.rawValue).joined(separator: ",")
|
||||
parameters["ImageTypeLimit"] = imageTypeLimit
|
||||
parameters["IncludeItemTypes"] = itemTypes.map(\.rawValue).joined(separator: ",")
|
||||
parameters["ParentId"] = parentID
|
||||
parameters["Recursive"] = recursive
|
||||
parameters["SortBy"] = sort?.rawValue
|
||||
parameters["SortOrder"] = asc?.rawValue
|
||||
parameters["Genres"] = genres.joined(separator: ",")
|
||||
parameters["PersonIds"] = personIds.joined(separator: ",")
|
||||
parameters["OfficialRatings"] = officialRatings.joined(separator: ",")
|
||||
return parameters
|
||||
}
|
||||
}
|
||||
|
||||
enum JellyfinAPI {
|
||||
case items(globalData: GlobalData, filter: Filter, page: Int)
|
||||
case search(globalData: GlobalData, filter: Filter, searchQuery: String, page: Int)
|
||||
}
|
||||
|
||||
extension JellyfinAPI: TargetType {
|
||||
|
||||
var baseURL: URL {
|
||||
switch self {
|
||||
case let .items(global, _, _),
|
||||
let .search(global, _, _, _):
|
||||
return URL(string: global.server?.baseURI ?? "")!
|
||||
}
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case let .items(global, _, _),
|
||||
let .search(global, _, _, _):
|
||||
return "/Users/\(global.user?.user_id ?? "")/Items"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .items, .search:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
var sampleData: Data {
|
||||
"{".data(using: .utf8)!
|
||||
}
|
||||
|
||||
var task: Task {
|
||||
switch self {
|
||||
case let .search(_, filter, searchQuery, page):
|
||||
var parameters = filter.toParamters
|
||||
parameters["searchTerm"] = searchQuery
|
||||
parameters["StartIndex"] = (page - 1) * 100
|
||||
parameters["Limit"] = 100
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.jellyfin)
|
||||
case let .items(_, filter, page):
|
||||
var parameters = filter.toParamters
|
||||
parameters["StartIndex"] = (page - 1) * 100
|
||||
parameters["Limit"] = 100
|
||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.jellyfin)
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String: String]? {
|
||||
switch self {
|
||||
case let .items(global, _, _),
|
||||
let .search(global, _, _, _):
|
||||
var headers = [String: String]()
|
||||
headers["Content-Type"] = "application/json"
|
||||
headers["Accept"] = "application/json"
|
||||
headers["X-Emby-Authorization"] = global.authHeader
|
||||
return headers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLEncoding {
|
||||
|
||||
static var jellyfin: URLEncoding {
|
||||
URLEncoding(destination: .methodDependent, arrayEncoding: .noBrackets, boolEncoding: .literal)
|
||||
}
|
||||
}
|
|
@ -8,121 +8,149 @@
|
|||
import SwiftUI
|
||||
|
||||
import KeychainSwift
|
||||
import SwiftyRequest
|
||||
import SwiftyJSON
|
||||
import Sentry
|
||||
import SDWebImageSwiftUI
|
||||
import Sentry
|
||||
import SwiftyJSON
|
||||
import SwiftyRequest
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
@StateObject private var globalData = GlobalData()
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var orientationInfo: OrientationInfo
|
||||
@StateObject
|
||||
private var globalData = GlobalData()
|
||||
@EnvironmentObject
|
||||
var jsi: justSignedIn
|
||||
|
||||
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)]) private var servers: FetchedResults<Server>
|
||||
|
||||
@FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)]) private var savedUsers: FetchedResults<SignedInUser>
|
||||
|
||||
@State private var needsToSelectServer = false;
|
||||
@State private var isSignInErrored = false;
|
||||
@State private var isNetworkErrored = false;
|
||||
@State private var isLoading = false;
|
||||
@State private var tabSelection: String = "Home";
|
||||
@State private var libraries: [String] = [];
|
||||
@State private var library_names: [String: String] = [:];
|
||||
@State private var librariesShowRecentlyAdded: [String] = [];
|
||||
@State private var libraryPrefillID: String = "";
|
||||
@State private var showSettingsPopover: Bool = false;
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
@FetchRequest(entity: Server.entity(),
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
|
||||
private var servers: FetchedResults<Server>
|
||||
|
||||
@FetchRequest(entity: SignedInUser.entity(),
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username,
|
||||
ascending: true)])
|
||||
private var savedUsers: FetchedResults<SignedInUser>
|
||||
|
||||
@State
|
||||
private var needsToSelectServer = false
|
||||
@State
|
||||
private var isSignInErrored = false
|
||||
@State
|
||||
private var isNetworkErrored = false
|
||||
@State
|
||||
private var isLoading = false
|
||||
@State
|
||||
private var tabSelection: String = "Home"
|
||||
@State
|
||||
private var libraries: [String] = []
|
||||
@State
|
||||
private var library_names: [String: String] = [:]
|
||||
@State
|
||||
private var librariesShowRecentlyAdded: [String] = []
|
||||
@State
|
||||
private var libraryPrefillID: String = ""
|
||||
@State
|
||||
private var showSettingsPopover: Bool = false
|
||||
@State
|
||||
private var viewDidLoad: Bool = false
|
||||
|
||||
func startup() {
|
||||
let size = UIScreen.main.bounds.size
|
||||
if size.width < size.height {
|
||||
orientationInfo.orientation = .portrait;
|
||||
orientationInfo.orientation = .portrait
|
||||
} else {
|
||||
orientationInfo.orientation = .landscape;
|
||||
orientationInfo.orientation = .landscape
|
||||
}
|
||||
|
||||
if(_viewDidLoad.wrappedValue) {
|
||||
|
||||
if _viewDidLoad.wrappedValue {
|
||||
return
|
||||
}
|
||||
|
||||
_viewDidLoad.wrappedValue = true;
|
||||
|
||||
_viewDidLoad.wrappedValue = true
|
||||
SentrySDK.start { options in
|
||||
options.dsn = "https://75ac77d6af4d406eb989f3d8ef0f119f@o513670.ingest.sentry.io/5778242"
|
||||
options.debug = false // Enabled debug when first installing is always helpful
|
||||
options.tracesSampleRate = 1.0
|
||||
options.releaseName = "ios-" + (Bundle.main.infoDictionary?["CFBundleVersion"] as! String);
|
||||
options.releaseName = "ios-" + (Bundle.main.infoDictionary?["CFBundleVersion"] as! String)
|
||||
options.enableOutOfMemoryTracking = true
|
||||
}
|
||||
|
||||
|
||||
let cache = SDImageCache(namespace: "tiny")
|
||||
cache.config.maxMemoryCost = 125 * 1024 * 1024 // 125MB memory
|
||||
cache.config.maxDiskSize = 1000 * 1024 * 1024 // 1000MB disk
|
||||
SDImageCachesManager.shared.addCache(cache)
|
||||
SDWebImageManager.defaultImageCache = SDImageCachesManager.shared
|
||||
|
||||
|
||||
_libraries.wrappedValue = []
|
||||
_library_names.wrappedValue = [:]
|
||||
_librariesShowRecentlyAdded.wrappedValue = []
|
||||
if(servers.isEmpty) {
|
||||
_isLoading.wrappedValue = false;
|
||||
_needsToSelectServer.wrappedValue = true;
|
||||
if servers.isEmpty {
|
||||
_isLoading.wrappedValue = false
|
||||
_needsToSelectServer.wrappedValue = true
|
||||
} else {
|
||||
_isLoading.wrappedValue = true;
|
||||
let savedUser = savedUsers[0];
|
||||
_isLoading.wrappedValue = true
|
||||
let savedUser = savedUsers[0]
|
||||
|
||||
let keychain = KeychainSwift();
|
||||
if(keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil) {
|
||||
let keychain = KeychainSwift()
|
||||
if keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil {
|
||||
_globalData.wrappedValue.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? ""
|
||||
_globalData.wrappedValue.server = servers[0]
|
||||
_globalData.wrappedValue.user = savedUser
|
||||
}
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
||||
globalData.authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(globalData.user?.device_uuid ?? "")\", Version=\"\(appVersion ?? "0.0.1")\", Token=\"\(globalData.authToken)\"";
|
||||
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var header = "MediaBrowser "
|
||||
header.append("Client=\"SwiftFin\",")
|
||||
header.append("Device=\"\(UIDevice.current.name.removeRegexMatches(pattern: "[^\\w\\s]"))\",")
|
||||
header.append("DeviceId=\"\(globalData.user?.device_uuid ?? "")\",")
|
||||
header.append("Version=\"\(appVersion ?? "0.0.1")\",")
|
||||
header.append("Token=\"\(globalData.authToken)\"")
|
||||
globalData.authHeader = header
|
||||
|
||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/Me")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request.responseData { (result: Result<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success( let resp):
|
||||
case let .success(resp):
|
||||
do {
|
||||
let json = try JSON(data: resp.body)
|
||||
let array2 = json["Configuration"]["LatestItemsExcludes"].arrayObject as? [String] ?? []
|
||||
|
||||
let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Views")
|
||||
|
||||
let request2 = RestRequest(method: .get,
|
||||
url: (globalData.server?.baseURI ?? "") +
|
||||
"/Users/\(globalData.user?.user_id ?? "")/Views")
|
||||
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request2.contentType = "application/json"
|
||||
request2.acceptType = "application/json"
|
||||
|
||||
request2.responseData() { (result2: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request2.responseData { (result2: Result<RestResponse<Data>, RestError>) in
|
||||
switch result2 {
|
||||
case .success( let resp):
|
||||
case let .success(resp):
|
||||
do {
|
||||
let json2 = try JSON(data: resp.body)
|
||||
for (_,item2):(String, JSON) in json2["Items"] {
|
||||
for (_, item2): (String, JSON) in json2["Items"] {
|
||||
_library_names.wrappedValue[item2["Id"].string ?? ""] = item2["Name"].string ?? ""
|
||||
}
|
||||
|
||||
for (_,item2):(String, JSON) in json2["Items"] {
|
||||
if(item2["CollectionType"].string == "tvshows" || item2["CollectionType"].string == "movies") {
|
||||
|
||||
for (_, item2): (String, JSON) in json2["Items"] {
|
||||
if item2["CollectionType"].string == "tvshows" || item2["CollectionType"].string == "movies" {
|
||||
_libraries.wrappedValue.append(item2["Id"].string ?? "")
|
||||
_librariesShowRecentlyAdded.wrappedValue.append(item2["Id"].string ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_librariesShowRecentlyAdded.wrappedValue = _libraries.wrappedValue.filter { element in
|
||||
return !array2.contains(element)
|
||||
!array2.contains(element)
|
||||
}
|
||||
|
||||
|
||||
_libraries.wrappedValue.forEach { library in
|
||||
if(_library_names.wrappedValue[library] == nil) {
|
||||
if _library_names.wrappedValue[library] == nil {
|
||||
_libraries.wrappedValue.removeAll { ele in
|
||||
if(library == ele) {
|
||||
if library == ele {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
@ -130,39 +158,32 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dump(_libraries.wrappedValue)
|
||||
dump(_librariesShowRecentlyAdded.wrappedValue)
|
||||
dump(_library_names.wrappedValue)
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure(let error):
|
||||
} catch {}
|
||||
case let .failure(error):
|
||||
SentrySDK.capture(error: error)
|
||||
break
|
||||
}
|
||||
let defaults = UserDefaults.standard;
|
||||
if(defaults.integer(forKey: "InNetworkBandwidth") == 0) {
|
||||
defaults.setValue(40000000, forKey: "InNetworkBandwidth")
|
||||
let defaults = UserDefaults.standard
|
||||
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
|
||||
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
|
||||
}
|
||||
if(defaults.integer(forKey: "OutOfNetworkBandwidth") == 0) {
|
||||
defaults.setValue(40000000, forKey: "OutOfNetworkBandwidth")
|
||||
if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 {
|
||||
defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth")
|
||||
}
|
||||
_isLoading.wrappedValue = false;
|
||||
_isLoading.wrappedValue = false
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure( let error):
|
||||
if(error.response?.status.code == 401) {
|
||||
_isLoading.wrappedValue = false;
|
||||
_isSignInErrored.wrappedValue = true;
|
||||
} catch {}
|
||||
case let .failure(error):
|
||||
if error.response?.status.code == 401 {
|
||||
_isLoading.wrappedValue = false
|
||||
_isSignInErrored.wrappedValue = true
|
||||
} else {
|
||||
SentrySDK.capture(error: error)
|
||||
_isLoading.wrappedValue = false;
|
||||
_isNetworkErrored.wrappedValue = true;
|
||||
_isLoading.wrappedValue = false
|
||||
_isNetworkErrored.wrappedValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,34 +191,39 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if(needsToSelectServer) {
|
||||
NavigationView() {
|
||||
if needsToSelectServer {
|
||||
NavigationView {
|
||||
ConnectToServerView(isActive: $needsToSelectServer)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(globalData)
|
||||
} else if(isSignInErrored) {
|
||||
NavigationView() {
|
||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored)
|
||||
} else if isSignInErrored {
|
||||
NavigationView {
|
||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
||||
reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(globalData)
|
||||
} else {
|
||||
if(!jsi.did) {
|
||||
if !jsi.did {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView() {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
ScrollView() {
|
||||
ScrollView {
|
||||
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 15)
|
||||
ContinueWatchingView()
|
||||
NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||
ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in
|
||||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
|
||||
HStack {
|
||||
Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold)
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
|
||||
Spacer()
|
||||
NavigationLink(destination: LibraryView(prefill: library_id, names: [library_id: library_names[library_id] ?? ""], libraries: [library_id], filter: "&SortBy=DateCreated&SortOrder=Descending")) {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filter: Filter(parentID: library_id)),
|
||||
title: library_names[library_id] ?? "")
|
||||
}) {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
}
|
||||
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
|
@ -210,29 +236,31 @@ struct ContentView: View {
|
|||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showSettingsPopover = true;
|
||||
showSettingsPopover = true
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}.fullScreenCover( isPresented: $showSettingsPopover) { SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) }
|
||||
}
|
||||
.fullScreenCover(isPresented: $showSettingsPopover) {
|
||||
SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem({
|
||||
.tabItem {
|
||||
Text("Home")
|
||||
Image(systemName: "house")
|
||||
})
|
||||
}
|
||||
.tag("Home")
|
||||
|
||||
NavigationView() {
|
||||
LibraryView(prefill: "", names: library_names, libraries: libraries)
|
||||
NavigationView {
|
||||
LibraryListView(viewModel: .init(libraryNames: library_names, libraryIDs: libraries))
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem({
|
||||
.tabItem {
|
||||
Text("All Media")
|
||||
Image(systemName: "folder")
|
||||
})
|
||||
}
|
||||
.tag("All Media")
|
||||
}
|
||||
}
|
||||
|
@ -243,13 +271,13 @@ struct ContentView: View {
|
|||
}
|
||||
} else {
|
||||
Text("Signing in...")
|
||||
.onAppear(perform: {
|
||||
DispatchQueue.main.async { [self] in
|
||||
_viewDidLoad.wrappedValue = false
|
||||
usleep(500000);
|
||||
self.jsi.did = false;
|
||||
}
|
||||
})
|
||||
.onAppear(perform: {
|
||||
DispatchQueue.main.async { [self] in
|
||||
_viewDidLoad.wrappedValue = false
|
||||
usleep(500_000)
|
||||
self.jsi.did = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ struct ContinueWatchingView: View {
|
|||
let json = try JSON(data: body)
|
||||
for (_,item):(String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
var itemObj = ResumeItem()
|
||||
if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) {
|
||||
//portrait; use backdrop instead
|
||||
itemObj.Image = item["BackdropImageTags"][0].string ?? ""
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// LibraryListViewModel.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/28.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CombineMoya
|
||||
import Foundation
|
||||
import Moya
|
||||
import SwiftyJSON
|
||||
|
||||
final class LibraryListViewModel: ObservableObject {
|
||||
fileprivate var provider =
|
||||
MoyaProvider<JellyfinAPI>(plugins: [NetworkLoggerPlugin()])
|
||||
|
||||
@Published
|
||||
var libraryIDs = [String]()
|
||||
@Published
|
||||
var libraryNames = [String: String]()
|
||||
|
||||
fileprivate var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(libraryNames: [String: String], libraryIDs: [String]) {
|
||||
self.libraryIDs = libraryIDs
|
||||
self.libraryNames = libraryNames
|
||||
refresh()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
libraryIDs.append("favorites")
|
||||
libraryNames["favorites"] = "Favorites"
|
||||
|
||||
libraryIDs.append("genres")
|
||||
libraryNames["genres"] = "Genres - WIP"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// LibraryViewModel.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/27.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CombineMoya
|
||||
import Foundation
|
||||
import Moya
|
||||
import SwiftyJSON
|
||||
|
||||
final class LibraryViewModel: ObservableObject {
|
||||
fileprivate var provider =
|
||||
MoyaProvider<JellyfinAPI>(plugins: [NetworkLoggerPlugin()])
|
||||
|
||||
@Published
|
||||
var filter: Filter
|
||||
|
||||
@Published
|
||||
var items = [ResumeItem]()
|
||||
|
||||
@Published
|
||||
var isLoading: Bool = true
|
||||
|
||||
@Published
|
||||
var isHiddenPreviousButton = true
|
||||
@Published
|
||||
var isHiddenNextButton = true
|
||||
|
||||
var page = 1
|
||||
|
||||
var globalData = GlobalData() {
|
||||
didSet {
|
||||
injectEnvironmentData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(filter: Filter = Filter()) {
|
||||
self.filter = filter
|
||||
}
|
||||
|
||||
fileprivate func injectEnvironmentData() {
|
||||
cancellables.removeAll()
|
||||
|
||||
$filter
|
||||
.sink(receiveValue: requestInitItems(_:))
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func requestNextPage() {
|
||||
page += 1
|
||||
requestItems(filter)
|
||||
}
|
||||
|
||||
func requestPreviousPage() {
|
||||
page -= 1
|
||||
requestItems(filter)
|
||||
}
|
||||
|
||||
func requestInitItems(_ filter: Filter) {
|
||||
page = 1
|
||||
requestItems(filter)
|
||||
}
|
||||
|
||||
fileprivate func requestItems(_ filter: Filter) {
|
||||
print("ASDASDA")
|
||||
print(globalData.authHeader)
|
||||
isLoading = true
|
||||
provider.requestPublisher(.items(globalData: globalData, filter: filter, page: page))
|
||||
// .map(ResumeItem.self) TO DO
|
||||
.print()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { response -> ([ResumeItem], Int) in
|
||||
let body = response.data
|
||||
var totalCount = 0
|
||||
var innerItems = [ResumeItem]()
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
totalCount = json["TotalRecordCount"].int ?? 0
|
||||
for (_, item): (String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
var itemObj = ResumeItem()
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
if itemObj.Type == "Series" {
|
||||
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = nil
|
||||
itemObj.SeasonId = nil
|
||||
itemObj.SeriesId = nil
|
||||
itemObj.SeriesName = nil
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
} else {
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
|
||||
itemObj.SeasonId = item["SeasonId"].string ?? nil
|
||||
itemObj.SeriesId = item["SeriesId"].string ?? nil
|
||||
itemObj.SeriesName = item["SeriesName"].string ?? nil
|
||||
}
|
||||
itemObj.Watched = item["UserData"]["Played"].bool ?? false
|
||||
|
||||
innerItems.append(itemObj)
|
||||
}
|
||||
} catch {}
|
||||
return (innerItems, totalCount)
|
||||
}
|
||||
.sink(receiveCompletion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.isLoading = false
|
||||
}, receiveValue: { [weak self] items, count in
|
||||
guard let self = self else { return }
|
||||
if count > 100 {
|
||||
if self.page > 1 {
|
||||
self.isHiddenPreviousButton = false
|
||||
}
|
||||
if count > (self.page * 100) {
|
||||
self.isHiddenNextButton = false
|
||||
}
|
||||
} else {
|
||||
self.isHiddenNextButton = true
|
||||
self.isHiddenPreviousButton = true
|
||||
}
|
||||
|
||||
self.items = items
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// LibrarySearchViewModel.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/27.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CombineMoya
|
||||
import Foundation
|
||||
import Moya
|
||||
import SwiftyJSON
|
||||
|
||||
final class LibrarySearchViewModel: ObservableObject {
|
||||
fileprivate var provider = MoyaProvider<JellyfinAPI>(plugins: [NetworkLoggerPlugin()])
|
||||
|
||||
var filter: Filter
|
||||
|
||||
@Published
|
||||
var items = [ResumeItem]()
|
||||
|
||||
@Published
|
||||
var searchQuery = ""
|
||||
@Published
|
||||
var isLoading: Bool = true
|
||||
|
||||
var page = 1
|
||||
|
||||
var globalData = GlobalData() {
|
||||
didSet {
|
||||
injectEnvironmentData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(filter: Filter) {
|
||||
self.filter = filter
|
||||
}
|
||||
|
||||
fileprivate func injectEnvironmentData() {
|
||||
cancellables.removeAll()
|
||||
|
||||
$searchQuery
|
||||
.debounce(for: 0.25, scheduler: DispatchQueue.main)
|
||||
.sink(receiveValue: requestSearch(query:))
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
fileprivate func requestSearch(query: String) {
|
||||
isLoading = true
|
||||
provider.requestPublisher(.search(globalData: globalData, filter: filter, searchQuery: query, page: page))
|
||||
// .map(ResumeItem.self) TO DO
|
||||
.print()
|
||||
.sink(receiveCompletion: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.isLoading = false
|
||||
}, receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let body = response.data
|
||||
var innerItems = [ResumeItem]()
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
for (_, item): (String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
var itemObj = ResumeItem()
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
if itemObj.Type == "Series" {
|
||||
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = nil
|
||||
itemObj.SeasonId = nil
|
||||
itemObj.SeriesId = nil
|
||||
itemObj.SeriesName = nil
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
} else {
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
|
||||
itemObj.SeasonId = item["SeasonId"].string ?? nil
|
||||
itemObj.SeriesId = item["SeriesId"].string ?? nil
|
||||
itemObj.SeriesName = item["SeriesName"].string ?? nil
|
||||
}
|
||||
itemObj.Watched = item["UserData"]["Played"].bool ?? false
|
||||
|
||||
innerItems.append(itemObj)
|
||||
}
|
||||
} catch {}
|
||||
self.items = innerItems
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -5,91 +5,107 @@
|
|||
// Created by Aiden Vigue on 5/13/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftyRequest
|
||||
import SwiftyJSON
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
import SwiftyRequest
|
||||
|
||||
struct EpisodeItemView: View {
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject private var playbackInfo: ItemPlayback
|
||||
var item: ResumeItem;
|
||||
var fullItem: DetailItem;
|
||||
@EnvironmentObject
|
||||
private var globalData: GlobalData
|
||||
@EnvironmentObject
|
||||
private var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject
|
||||
private var playbackInfo: ItemPlayback
|
||||
var item: ResumeItem
|
||||
var fullItem: DetailItem
|
||||
|
||||
@State private var isLoading: Bool = true;
|
||||
@State private var progressString: String = "";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
@State private var watched: Bool = false {
|
||||
@State
|
||||
private var isLoading: Bool = true
|
||||
@State
|
||||
private var progressString: String = ""
|
||||
@State
|
||||
private var viewDidLoad: Bool = false
|
||||
|
||||
@State
|
||||
private var watched: Bool = false {
|
||||
didSet {
|
||||
if(watched == true) {
|
||||
if watched == true {
|
||||
let date = Date()
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
||||
let request = RestRequest(method: .post,
|
||||
url: (globalData.server?.baseURI ?? "") +
|
||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request.responseData { (_: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
} else {
|
||||
let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
||||
let request = RestRequest(method: .delete,
|
||||
url: (globalData.server?.baseURI ?? "") +
|
||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request.responseData { (_: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@State private var favorite: Bool = false {
|
||||
didSet {
|
||||
if(favorite == true) {
|
||||
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
} else {
|
||||
let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init(item: ResumeItem) {
|
||||
self.item = item;
|
||||
fullItem = DetailItem();
|
||||
}
|
||||
|
||||
|
||||
@State
|
||||
private var favorite: Bool = false {
|
||||
didSet {
|
||||
if favorite == true {
|
||||
let request = RestRequest(method: .post,
|
||||
url: (globalData.server?.baseURI ?? "") +
|
||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData { (_: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
} else {
|
||||
let request = RestRequest(method: .delete,
|
||||
url: (globalData.server?.baseURI ?? "") +
|
||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData { (_: Result<RestResponse<Data>, RestError>) in
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(item: ResumeItem) {
|
||||
self.item = item
|
||||
self.fullItem = DetailItem()
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
if(_viewDidLoad.wrappedValue == true) {
|
||||
if _viewDidLoad.wrappedValue == true {
|
||||
return
|
||||
}
|
||||
_viewDidLoad.wrappedValue = true;
|
||||
_viewDidLoad.wrappedValue = true
|
||||
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
|
||||
|
||||
|
||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request.responseData { (result: Result<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
case let .success(response):
|
||||
let body = response.body
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
|
@ -110,225 +126,260 @@ struct EpisodeItemView: View {
|
|||
fullItem.SeriesName = json["SeriesName"].string ?? nil
|
||||
fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0)
|
||||
fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13"
|
||||
fullItem.Watched = json["UserData"]["Played"].bool ?? false;
|
||||
fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0);
|
||||
fullItem.CriticRating = String(json["CriticRating"].int ?? 0);
|
||||
fullItem.Watched = json["UserData"]["Played"].bool ?? false
|
||||
fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0)
|
||||
fullItem.CriticRating = String(json["CriticRating"].int ?? 0)
|
||||
fullItem.ParentId = json["ParentId"].string ?? ""
|
||||
fullItem.ParentBackdropItemId = json["ParentBackdropItemId"].string ?? ""
|
||||
//People
|
||||
// People
|
||||
fullItem.Directors = []
|
||||
fullItem.Studios = []
|
||||
fullItem.Writers = []
|
||||
fullItem.Cast = []
|
||||
fullItem.Genres = []
|
||||
|
||||
for (_,person):(String, JSON) in json["People"] {
|
||||
if(person["Type"].stringValue == "Director") {
|
||||
fullItem.Directors.append(person["Name"].string ?? "");
|
||||
} else if(person["Type"].stringValue == "Writer") {
|
||||
fullItem.Writers.append(person["Name"].string ?? "");
|
||||
} else if(person["Type"].stringValue == "Actor") {
|
||||
let cast = CastMember();
|
||||
cast.Name = person["Name"].string ?? "";
|
||||
cast.Id = person["Id"].string ?? "";
|
||||
let imageTag = person["PrimaryImageTag"].string ?? "";
|
||||
cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "";
|
||||
cast.Role = person["Role"].string ?? "";
|
||||
cast.Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
|
||||
fullItem.Cast.append(cast);
|
||||
|
||||
for (_, person): (String, JSON) in json["People"] {
|
||||
if person["Type"].stringValue == "Director" {
|
||||
fullItem.Directors.append(person["Name"].string ?? "")
|
||||
} else if person["Type"].stringValue == "Writer" {
|
||||
fullItem.Writers.append(person["Name"].string ?? "")
|
||||
} else if person["Type"].stringValue == "Actor" {
|
||||
let cast = CastMember()
|
||||
cast.Name = person["Name"].string ?? ""
|
||||
cast.Id = person["Id"].string ?? ""
|
||||
let imageTag = person["PrimaryImageTag"].string ?? ""
|
||||
cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""
|
||||
cast.Role = person["Role"].string ?? ""
|
||||
cast
|
||||
.Image =
|
||||
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
|
||||
fullItem.Cast.append(cast)
|
||||
}
|
||||
}
|
||||
|
||||
//Studios
|
||||
for (_,studio):(String, JSON) in json["Studios"] {
|
||||
fullItem.Studios.append(studio["Name"].string ?? "");
|
||||
|
||||
// Studios
|
||||
for (_, studio): (String, JSON) in json["Studios"] {
|
||||
fullItem.Studios.append(studio["Name"].string ?? "")
|
||||
}
|
||||
|
||||
//Genres
|
||||
for (_,genre):(String, JSON) in json["GenreItems"] {
|
||||
|
||||
// Genres
|
||||
for (_, genre): (String, JSON) in json["GenreItems"] {
|
||||
let tmpGenre = IVGenre()
|
||||
tmpGenre.Id = genre["Id"].string ?? "";
|
||||
tmpGenre.Name = genre["Name"].string ?? "";
|
||||
fullItem.Genres.append(tmpGenre);
|
||||
tmpGenre.Id = genre["Id"].string ?? ""
|
||||
tmpGenre.Name = genre["Name"].string ?? ""
|
||||
fullItem.Genres.append(tmpGenre)
|
||||
}
|
||||
|
||||
|
||||
_watched.wrappedValue = fullItem.Watched
|
||||
_favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false;
|
||||
|
||||
//Process runtime
|
||||
let seconds: Int = ((json["RunTimeTicks"].int ?? 0)/10000000)
|
||||
fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0;
|
||||
let hours = (seconds/3600)
|
||||
let minutes = ((seconds - (hours * 3600))/60)
|
||||
if(hours != 0) {
|
||||
_favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false
|
||||
|
||||
// Process runtime
|
||||
let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000)
|
||||
fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0
|
||||
let hours = (seconds / 3600)
|
||||
let minutes = ((seconds - (hours * 3600)) / 60)
|
||||
if hours != 0 {
|
||||
fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))"
|
||||
} else {
|
||||
fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
|
||||
}
|
||||
|
||||
if(fullItem.Progress != 0) {
|
||||
let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress)/10000000
|
||||
let proghours = Int(remainingSecs/3600)
|
||||
let progminutes = Int((Int(remainingSecs) - (proghours * 3600))/60)
|
||||
if(proghours != 0) {
|
||||
|
||||
if fullItem.Progress != 0 {
|
||||
let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress) / 10_000_000
|
||||
let proghours = Int(remainingSecs / 3600)
|
||||
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
|
||||
if proghours != 0 {
|
||||
_progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))"
|
||||
} else {
|
||||
_progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure(let error):
|
||||
} catch {}
|
||||
case let .failure(error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
_isLoading.wrappedValue = false;
|
||||
_isLoading.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
VStack(alignment:.leading) {
|
||||
if(!isLoading) {
|
||||
if(orientationInfo.orientation == .portrait) {
|
||||
VStack(alignment: .leading) {
|
||||
if !isLoading {
|
||||
if orientationInfo.orientation == .portrait {
|
||||
GeometryReader { geometry in
|
||||
VStack() {
|
||||
VStack {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
Image(uiImage: UIImage(blurHash: fullItem
|
||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||
.BackdropBlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
|
||||
.trailing,
|
||||
height: UIDevice.current
|
||||
.userInterfaceIdiom == .pad ? 350 :
|
||||
(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
|
||||
.trailing) * 0.5625)
|
||||
}
|
||||
|
||||
|
||||
.opacity(0.3)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
||||
height: UIDevice.current
|
||||
.userInterfaceIdiom == .pad ? 350 :
|
||||
(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) *
|
||||
0.5625)
|
||||
.shadow(radius: 5)
|
||||
.overlay(
|
||||
HStack() {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
}.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
Text(fullItem.Name).font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.offset(y: -4)
|
||||
HStack() {
|
||||
Text(String(fullItem.ProductionYear)).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.overlay(HStack {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: fullItem
|
||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||
fullItem.PosterBlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
}.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
Text(fullItem.Name).font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.offset(y: -4)
|
||||
HStack {
|
||||
Text(String(fullItem.ProductionYear)).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
Text(fullItem.Runtime).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
if fullItem.OfficialRating != "" {
|
||||
Text(fullItem.OfficialRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
Text(fullItem.Runtime).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
if(fullItem.OfficialRating != "") {
|
||||
Text(fullItem.OfficialRating).font(.subheadline)
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
if fullItem.CommunityRating != "0" {
|
||||
HStack {
|
||||
Image(systemName: "star").foregroundColor(.secondary)
|
||||
Text(fullItem.CommunityRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1)
|
||||
)
|
||||
.offset(x: -7, y: 0.7)
|
||||
}
|
||||
if(fullItem.CommunityRating != "0") {
|
||||
HStack() {
|
||||
Image(systemName: "star").foregroundColor(.secondary)
|
||||
Text(fullItem.CommunityRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.offset(x: -7, y: 0.7)
|
||||
}
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}.frame(maxWidth: .infinity, alignment: .leading).offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46).padding(.trailing, 16)
|
||||
}.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
|
||||
, alignment: .bottomLeading)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46)
|
||||
.padding(.trailing, 16)
|
||||
}.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40),
|
||||
alignment: .bottomLeading)
|
||||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
//Play button
|
||||
HStack {
|
||||
// Play button
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
self.playbackInfo.itemToPlay = fullItem
|
||||
self.playbackInfo.shouldPlay = true
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
HStack {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left")
|
||||
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
}
|
||||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.cornerRadius(10)
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
HStack() {
|
||||
Button() {
|
||||
HStack {
|
||||
Button {
|
||||
favorite.toggle()
|
||||
} label: {
|
||||
if(!favorite) {
|
||||
if !favorite {
|
||||
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
} else {
|
||||
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20))
|
||||
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
Button() {
|
||||
Button {
|
||||
watched.toggle()
|
||||
} label: {
|
||||
if(watched) {
|
||||
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
if watched {
|
||||
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
|
||||
.font(.system(size: 20))
|
||||
} else {
|
||||
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16).padding(.trailing,16)
|
||||
ScrollView() {
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
if(fullItem.Tagline != "") {
|
||||
Text(fullItem.Tagline).font(.body).italic().padding(.top, 7).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing,16)
|
||||
if fullItem.Tagline != "" {
|
||||
Text(fullItem.Tagline).font(.body).italic().padding(.top, 7)
|
||||
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing,16)
|
||||
if(fullItem.Genres.count != 0) {
|
||||
Text(fullItem.Overview).font(.footnote).padding(.top, 3)
|
||||
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
|
||||
.padding(.trailing, 16)
|
||||
if !fullItem.Genres.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack() {
|
||||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(fullItem.Genres, id: \.Id) {genre in
|
||||
NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) {
|
||||
ForEach(fullItem.Genres, id: \.Id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filter: Filter(genres: [
|
||||
genre
|
||||
.Name,
|
||||
])), title: genre.Name)
|
||||
}) {
|
||||
Text(genre.Name).font(.footnote)
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16).padding(.trailing,16)
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
}
|
||||
if(fullItem.Cast.count != 0) {
|
||||
if !fullItem.Cast.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
VStack() {
|
||||
Spacer().frame(height: 8);
|
||||
HStack() {
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(fullItem.Cast, id: \.Id) { cast in
|
||||
NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) {
|
||||
VStack() {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filter: Filter(personIds: [
|
||||
cast
|
||||
.Id,
|
||||
])), title: cast.Name)
|
||||
}) {
|
||||
VStack {
|
||||
WebImage(url: cast.Image)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
Image(uiImage: UIImage(blurHash: cast
|
||||
.ImageBlurHash == "" ?
|
||||
"W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||
cast.ImageBlurHash,
|
||||
size: CGSize(width: 16,
|
||||
height: 16))!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
|
@ -337,9 +388,11 @@ struct EpisodeItemView: View {
|
|||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
.frame(width: 100).foregroundColor(Color.primary)
|
||||
if cast.Role != "" {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||
.foregroundColor(Color.secondary).frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,28 +403,33 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
}.padding(.top, -3)
|
||||
}
|
||||
if(fullItem.Directors.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Directors.isEmpty {
|
||||
HStack {
|
||||
Text("Directors:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing,16)
|
||||
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
if(fullItem.Writers.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Writers.isEmpty {
|
||||
HStack {
|
||||
Text("Writers:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing,16)
|
||||
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
if(fullItem.Studios.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Studios.isEmpty {
|
||||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing,16)
|
||||
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, 16)
|
||||
}
|
||||
Spacer().frame(height: 3)
|
||||
}
|
||||
}
|
||||
}.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0,
|
||||
trailing: 0))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -380,14 +438,21 @@ struct EpisodeItemView: View {
|
|||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
Image(uiImage: UIImage(blurHash: fullItem
|
||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||
.BackdropBlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
|
||||
.trailing,
|
||||
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets
|
||||
.bottom)
|
||||
}
|
||||
|
||||
|
||||
.opacity(0.3)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
||||
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.blur(radius:2)
|
||||
HStack() {
|
||||
|
@ -395,7 +460,10 @@ struct EpisodeItemView: View {
|
|||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
Image(uiImage: UIImage(blurHash: fullItem
|
||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||
fullItem.PosterBlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 120, height: 180)
|
||||
}
|
||||
|
@ -405,23 +473,24 @@ struct EpisodeItemView: View {
|
|||
.shadow(radius: 5)
|
||||
Spacer().frame(height: 15)
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
self.playbackInfo.itemToPlay = fullItem
|
||||
self.playbackInfo.shouldPlay = true
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
HStack {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left")
|
||||
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
}
|
||||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.cornerRadius(10)
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
}
|
||||
ScrollView() {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(fullItem.Name).font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -429,7 +498,7 @@ struct EpisodeItemView: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
.offset(x: 14, y: 0)
|
||||
Spacer().frame(height: 1)
|
||||
HStack() {
|
||||
HStack {
|
||||
Text(String(fullItem.ProductionYear)).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
|
@ -438,19 +507,17 @@ struct EpisodeItemView: View {
|
|||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
if(fullItem.OfficialRating != "") {
|
||||
if fullItem.OfficialRating != "" {
|
||||
Text(fullItem.OfficialRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1)
|
||||
)
|
||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.secondary, lineWidth: 1))
|
||||
}
|
||||
if(fullItem.CommunityRating != "0") {
|
||||
HStack() {
|
||||
if fullItem.CommunityRating != "0" {
|
||||
HStack {
|
||||
Image(systemName: "star").foregroundColor(.secondary)
|
||||
Text(fullItem.CommunityRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -461,59 +528,83 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
Spacer()
|
||||
}.frame(maxWidth: .infinity)
|
||||
.offset(x: 14)
|
||||
.offset(x: 14)
|
||||
}.frame(maxWidth: .infinity)
|
||||
Spacer()
|
||||
HStack() {
|
||||
Button() {
|
||||
HStack {
|
||||
Button {
|
||||
favorite.toggle()
|
||||
} label: {
|
||||
if(!favorite) {
|
||||
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
if !favorite {
|
||||
Image(systemName: "heart").foregroundColor(Color.primary)
|
||||
.font(.system(size: 20))
|
||||
} else {
|
||||
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20))
|
||||
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
Button() {
|
||||
Button {
|
||||
watched.toggle()
|
||||
} label: {
|
||||
if(watched) {
|
||||
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
if watched {
|
||||
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
|
||||
.font(.system(size: 20))
|
||||
} else {
|
||||
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20))
|
||||
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
if(fullItem.Tagline != "") {
|
||||
Text(fullItem.Tagline).font(.body).italic().padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
if fullItem.Tagline != "" {
|
||||
Text(fullItem.Tagline).font(.body).italic().padding(.top, 3)
|
||||
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
|
||||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
Text(fullItem.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(fullItem.Genres.count != 0) {
|
||||
Text(fullItem.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 !fullItem.Genres.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack() {
|
||||
HStack {
|
||||
Text("Genres:").font(.callout).fontWeight(.semibold)
|
||||
ForEach(fullItem.Genres, id: \.Id) {genre in
|
||||
NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) {
|
||||
ForEach(fullItem.Genres, id: \.Id) { genre in
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filter: Filter(genres: [
|
||||
genre
|
||||
.Name,
|
||||
])), title: genre.Name)
|
||||
}) {
|
||||
Text(genre.Name).font(.footnote)
|
||||
}
|
||||
}
|
||||
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}.padding(.leading, 16)
|
||||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
}
|
||||
if(fullItem.Cast.count != 0) {
|
||||
if !fullItem.Cast.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
VStack() {
|
||||
Spacer().frame(height: 8);
|
||||
HStack() {
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(fullItem.Cast, id: \.Id) { cast in
|
||||
NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) {
|
||||
VStack() {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filter: Filter(personIds: [
|
||||
cast
|
||||
.Id,
|
||||
])), title: cast.Name)
|
||||
}) {
|
||||
VStack {
|
||||
WebImage(url: cast.Image)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
Image(uiImage: UIImage(blurHash: cast
|
||||
.ImageBlurHash == "" ?
|
||||
"W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||
cast.ImageBlurHash,
|
||||
size: CGSize(width: 16,
|
||||
height: 16))!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
|
@ -522,9 +613,11 @@ struct EpisodeItemView: View {
|
|||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
.frame(width: 100).foregroundColor(Color.primary)
|
||||
if cast.Role != "" {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||
.foregroundColor(Color.secondary).frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -535,28 +628,35 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
}.padding(.top, -3)
|
||||
}
|
||||
if(fullItem.Directors.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Directors.isEmpty {
|
||||
HStack {
|
||||
Text("Directors:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16)
|
||||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
if(fullItem.Writers.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Writers.isEmpty {
|
||||
HStack {
|
||||
Text("Writers:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16)
|
||||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
if(fullItem.Studios.count != 0) {
|
||||
HStack() {
|
||||
if !fullItem.Studios.isEmpty {
|
||||
HStack {
|
||||
Text("Studios:").font(.callout).fontWeight(.semibold)
|
||||
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1)
|
||||
.foregroundColor(Color.secondary)
|
||||
}.padding(.leading, 16)
|
||||
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
Spacer().frame(height: 100);
|
||||
Spacer().frame(height: 100)
|
||||
}.frame(maxHeight: .infinity)
|
||||
}
|
||||
}.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55).edgesIgnoringSafeArea(.leading)
|
||||
}.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
.edgesIgnoringSafeArea(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -565,9 +665,9 @@ struct EpisodeItemView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("\(fullItem.Name) - S\(String(fullItem.ParentIndexNumber ?? 0)):E\(String(fullItem.IndexNumber ?? 0)) - \(fullItem.SeriesName ?? "")")
|
||||
}.onAppear(perform: loadData)
|
||||
.supportedOrientations(.allButUpsideDown)
|
||||
.overrideViewPreference(.unspecified)
|
||||
.preferredColorScheme(.none)
|
||||
.prefersHomeIndicatorAutoHidden(false)
|
||||
.supportedOrientations(.allButUpsideDown)
|
||||
.overrideViewPreference(.unspecified)
|
||||
.preferredColorScheme(.none)
|
||||
.prefersHomeIndicatorAutoHidden(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// String++.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
func removeRegexMatches(pattern: String, replaceWith: String = "") -> String {
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
|
||||
let range = NSRange(location: 0, length: count)
|
||||
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
||||
} catch { return self }
|
||||
}
|
||||
}
|
|
@ -53,25 +53,25 @@ struct ServerAuthByNameResponse: Codable {
|
|||
var AccessToken: String
|
||||
}
|
||||
|
||||
class ResumeItem: ObservableObject {
|
||||
@Published var Name: String = "";
|
||||
@Published var Id: String = "";
|
||||
@Published var IndexNumber: Int? = nil;
|
||||
@Published var ParentIndexNumber: Int? = nil;
|
||||
@Published var Image: String = "";
|
||||
@Published var ImageType: String = "";
|
||||
@Published var BlurHash: String = "";
|
||||
@Published var `Type`: String = "";
|
||||
@Published var SeasonId: String? = nil;
|
||||
@Published var SeriesId: String? = nil;
|
||||
@Published var SeriesName: String? = nil;
|
||||
@Published var ItemProgress: Double = 0;
|
||||
@Published var SeasonImage: String? = nil;
|
||||
@Published var SeasonImageType: String? = nil;
|
||||
@Published var SeasonImageBlurHash: String? = nil;
|
||||
@Published var ItemBadge: Int? = 0;
|
||||
@Published var ProductionYear: Int = 1999;
|
||||
@Published var Watched: Bool = false;
|
||||
struct ResumeItem {
|
||||
var Name: String = "";
|
||||
var Id: String = "";
|
||||
var IndexNumber: Int? = nil;
|
||||
var ParentIndexNumber: Int? = nil;
|
||||
var Image: String = "";
|
||||
var ImageType: String = "";
|
||||
var BlurHash: String = "";
|
||||
var `Type`: String = "";
|
||||
var SeasonId: String? = nil;
|
||||
var SeriesId: String? = nil;
|
||||
var SeriesName: String? = nil;
|
||||
var ItemProgress: Double = 0;
|
||||
var SeasonImage: String? = nil;
|
||||
var SeasonImageType: String? = nil;
|
||||
var SeasonImageBlurHash: String? = nil;
|
||||
var ItemBadge: Int? = 0;
|
||||
var ProductionYear: Int = 1999;
|
||||
var Watched: Bool = false;
|
||||
}
|
||||
|
||||
struct ServerMeResponse: Codable {
|
||||
|
|
|
@ -11,7 +11,7 @@ class justSignedIn: ObservableObject {
|
|||
@Published var did: Bool = false
|
||||
}
|
||||
|
||||
class GlobalData: ObservableObject {
|
||||
class GlobalData: ObservableObject {
|
||||
@Published var user: SignedInUser?
|
||||
@Published var authToken: String = ""
|
||||
@Published var server: Server?
|
||||
|
@ -19,6 +19,16 @@ class GlobalData: ObservableObject {
|
|||
@Published var isInNetwork: Bool = true;
|
||||
}
|
||||
|
||||
extension GlobalData: Equatable {
|
||||
|
||||
static func == (lhs: GlobalData, rhs: GlobalData) -> Bool {
|
||||
lhs.user == rhs.user
|
||||
&& lhs.authToken == rhs.authToken
|
||||
&& lhs.server == rhs.server
|
||||
&& lhs.authHeader == rhs.authHeader
|
||||
}
|
||||
}
|
||||
|
||||
extension UIDevice {
|
||||
var hasNotch: Bool {
|
||||
let bottom = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.safeAreaInsets.bottom ?? 0
|
||||
|
|
|
@ -47,7 +47,7 @@ struct LatestMediaView: View {
|
|||
let json = try JSON(data: body)
|
||||
for (_,item):(String, JSON) in json {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
var itemObj = ResumeItem()
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
|
|
|
@ -14,173 +14,150 @@ struct Genre: Hashable, Identifiable {
|
|||
var id: String { name }
|
||||
}
|
||||
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
|
||||
@State var library: String;
|
||||
@Binding var output: String;
|
||||
@State private var isLoading: Bool = true;
|
||||
@State private var onlyUnplayed: Bool = false;
|
||||
@State private var allGenres: [Genre] = [];
|
||||
@State private var selectedGenres: Set<Genre> = [];
|
||||
|
||||
@State private var allRatings: [Genre] = [];
|
||||
@State private var selectedRatings: Set<Genre> = [];
|
||||
@State private var sortBySelection: String = "SortName";
|
||||
@State private var sortOrder: String = "Descending";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
@Binding var close: Bool;
|
||||
|
||||
@Environment(\.presentationMode)
|
||||
var presentationMode
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
|
||||
@State
|
||||
var library: String
|
||||
|
||||
@Binding
|
||||
var filter: Filter
|
||||
@State
|
||||
private var isLoading: Bool = true
|
||||
@State
|
||||
private var onlyUnplayed: Bool = false
|
||||
@State
|
||||
private var allGenres: [Genre] = []
|
||||
@State
|
||||
private var selectedGenres: Set<Genre> = []
|
||||
|
||||
@State
|
||||
private var allRatings: [Genre] = []
|
||||
@State
|
||||
private var selectedRatings: Set<Genre> = []
|
||||
@State
|
||||
private var sortBySelection: String = "SortName"
|
||||
@State
|
||||
private var sortOrder: String = "Descending"
|
||||
@State
|
||||
private var viewDidLoad: Bool = false
|
||||
|
||||
func onAppear() {
|
||||
if(_viewDidLoad.wrappedValue == true) {
|
||||
if _viewDidLoad.wrappedValue == true {
|
||||
return
|
||||
}
|
||||
_viewDidLoad.wrappedValue = true;
|
||||
if(_output.wrappedValue.contains("&Filters=IsUnplayed")) {
|
||||
_onlyUnplayed.wrappedValue = true;
|
||||
_viewDidLoad.wrappedValue = true
|
||||
if filter.filterTypes.contains(.isUnplayed) {
|
||||
_onlyUnplayed.wrappedValue = true
|
||||
}
|
||||
if(_output.wrappedValue.contains("&Genres=")) {
|
||||
let genreString = _output.wrappedValue.components(separatedBy: "&Genres=")[1].components(separatedBy: "&")[0];
|
||||
for genre in genreString.components(separatedBy: "%7C") {
|
||||
_selectedGenres.wrappedValue.insert(Genre(name: genre.removingPercentEncoding ?? ""))
|
||||
}
|
||||
if !filter.genres.isEmpty {
|
||||
_selectedGenres.wrappedValue = Set(filter.genres.map { Genre(name: $0) })
|
||||
}
|
||||
if(_output.wrappedValue.contains("&OfficialRatings=")) {
|
||||
let ratingString = _output.wrappedValue.components(separatedBy: "&OfficialRatings=")[1].components(separatedBy: "&")[0];
|
||||
for rating in ratingString.components(separatedBy: "%7C") {
|
||||
_selectedRatings.wrappedValue.insert(Genre(name: rating.removingPercentEncoding ?? ""))
|
||||
}
|
||||
if !filter.officialRatings.isEmpty {
|
||||
_selectedRatings.wrappedValue = Set(filter.officialRatings.map { Genre(name: $0) })
|
||||
}
|
||||
let sortBy = _output.wrappedValue.components(separatedBy: "&SortBy=")[1].components(separatedBy: "&")[0];
|
||||
_sortBySelection.wrappedValue = sortBy;
|
||||
let sortOrder = _output.wrappedValue.components(separatedBy: "&SortOrder=")[1].components(separatedBy: "&")[0];
|
||||
_sortOrder.wrappedValue = sortOrder;
|
||||
|
||||
recalculateFilters()
|
||||
_sortBySelection.wrappedValue = filter.sort?.rawValue ?? sortBySelection
|
||||
_sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder
|
||||
|
||||
_allGenres.wrappedValue = []
|
||||
let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)"
|
||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
|
||||
request.responseData { (result: Result<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
case let .success(response):
|
||||
let body = response.body
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
let arr = json["Genres"].arrayObject as? [String] ?? []
|
||||
for genreName in arr {
|
||||
//print(genreName)
|
||||
// print(genreName)
|
||||
let genre = Genre(name: genreName)
|
||||
allGenres.append(genre)
|
||||
}
|
||||
|
||||
|
||||
let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? []
|
||||
for genreName in arr2 {
|
||||
//print(genreName)
|
||||
// print(genreName)
|
||||
let genre = Genre(name: genreName)
|
||||
allRatings.append(genre)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure(let error):
|
||||
} catch {}
|
||||
case let .failure(error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
isLoading = false;
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func recalculateFilters() {
|
||||
print("recalcFilters running");
|
||||
output = "";
|
||||
if(_onlyUnplayed.wrappedValue) {
|
||||
output = "&Filters=IsUnPlayed";
|
||||
}
|
||||
|
||||
if(selectedGenres.count != 0) {
|
||||
output += "&Genres="
|
||||
var genres: [String] = []
|
||||
for genre in selectedGenres {
|
||||
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
}
|
||||
output += genres.joined(separator: "%7C")
|
||||
}
|
||||
|
||||
if(selectedRatings.count != 0) {
|
||||
output += "&OfficialRatings="
|
||||
var genres: [String] = []
|
||||
for genre in selectedRatings {
|
||||
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
}
|
||||
output += genres.joined(separator: "%7C")
|
||||
}
|
||||
output += "&SortBy=\(sortBySelection)&SortOrder=\(sortOrder)"
|
||||
//print(output)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView() {
|
||||
NavigationView {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
Form {
|
||||
Toggle("Only show unplayed items", isOn: $onlyUnplayed)
|
||||
.onChange(of: onlyUnplayed) { tag in
|
||||
recalculateFilters()
|
||||
.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) { tag in
|
||||
recalculateFilters()
|
||||
}
|
||||
MultiSelector(
|
||||
label: "Parental Ratings",
|
||||
options: allRatings,
|
||||
optionToString: { $0.name },
|
||||
selected: $selectedRatings
|
||||
).onChange(of: selectedRatings) { tag in
|
||||
recalculateFilters()
|
||||
}
|
||||
|
||||
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) { tag in
|
||||
recalculateFilters()
|
||||
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) { tag in
|
||||
recalculateFilters()
|
||||
}.onChange(of: sortOrder) { order in
|
||||
guard let asc = ASC(rawValue: order) else { return }
|
||||
filter.asc = asc
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onAppear(perform: onAppear)
|
||||
.navigationBarTitle("Filters", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
close = false
|
||||
} label: {
|
||||
HStack() {
|
||||
Text("Back").font(.callout)
|
||||
.navigationBarTitle("Filters", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Back").font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// LibraryListView.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryListView: View {
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
@ObservedObject
|
||||
var viewModel: LibraryListViewModel
|
||||
|
||||
var body: some View {
|
||||
List(viewModel.libraryIDs, id: \.self) { id in
|
||||
switch id {
|
||||
case "favorites":
|
||||
NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filter: Filter(filterTypes: [.isFavorite])),
|
||||
title: viewModel.libraryNames[id] ?? "") }) {
|
||||
Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary)
|
||||
}
|
||||
case "genres":
|
||||
Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary)
|
||||
default:
|
||||
NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filter: Filter(parentID: id)),
|
||||
title: viewModel.libraryNames[id] ?? "") }) {
|
||||
Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("All Media")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
NavigationLink(destination: LazyView { LibrarySearchView(viewModel: .init(filter: .init())) }) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,193 +5,140 @@
|
|||
// Created by Aiden Vigue on 5/2/21.
|
||||
//
|
||||
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
import SwiftyRequest
|
||||
import SDWebImageSwiftUI
|
||||
|
||||
struct LibrarySearchView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
@ObservedObject
|
||||
var viewModel: LibrarySearchViewModel
|
||||
|
||||
@State var url: String;
|
||||
@Binding var close: Bool;
|
||||
@State var open: Bool = false;
|
||||
@State private var isLoading: Bool = true;
|
||||
@State private var onlyUnplayed: Bool = false;
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
@State var items: [ResumeItem] = []
|
||||
@State var linkedItem: ResumeItem = ResumeItem();
|
||||
@State var searchQuery: String = "" {
|
||||
didSet {
|
||||
self.onAppear();
|
||||
}
|
||||
};
|
||||
|
||||
func onAppear() {
|
||||
recalcTracks()
|
||||
_isLoading.wrappedValue = true;
|
||||
_items.wrappedValue = [];
|
||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + _url.wrappedValue + "&searchTerm=" + searchQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + (_url.wrappedValue.contains("SortBy") ? "" : "&SortBy=Name&SortOrder=Descending"))
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let body = response.body
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
for (_,item):(String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
if(itemObj.Type == "Series") {
|
||||
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = nil
|
||||
itemObj.SeasonId = nil
|
||||
itemObj.SeriesId = nil
|
||||
itemObj.SeriesName = nil
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
} else {
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
|
||||
itemObj.SeasonId = item["SeasonId"].string ?? nil
|
||||
itemObj.SeriesId = item["SeriesId"].string ?? nil
|
||||
itemObj.SeriesName = item["SeriesName"].string ?? nil
|
||||
}
|
||||
itemObj.Watched = item["UserData"]["Played"].bool ?? false
|
||||
@State
|
||||
private var tracks: [GridItem] = []
|
||||
|
||||
_items.wrappedValue.append(itemObj)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure(let error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
@Environment(\.verticalSizeClass)
|
||||
var verticalSizeClass: UserInterfaceSizeClass?
|
||||
@Environment(\.horizontalSizeClass)
|
||||
var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
||||
func onAppear() {
|
||||
guard viewModel.globalData != globalData else { return }
|
||||
recalcTracks()
|
||||
viewModel.globalData = globalData
|
||||
}
|
||||
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
||||
var isPortrait: Bool {
|
||||
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
func recalcTracks() {
|
||||
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125));
|
||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||
_tracks.wrappedValue = []
|
||||
for _ in (0..<trkCnt)
|
||||
{
|
||||
_tracks.wrappedValue.append(GridItem.init(.flexible()))
|
||||
for _ in 0 ..< trkCnt {
|
||||
_tracks.wrappedValue.append(GridItem(.flexible()))
|
||||
}
|
||||
}
|
||||
|
||||
@State private var tracks: [GridItem] = []
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
NavigationLink(destination: ItemView(item: linkedItem), isActive: $open) {
|
||||
EmptyView();
|
||||
};
|
||||
Spacer().frame(height:6);
|
||||
TextField("Search", text: $searchQuery, onEditingChanged: { _ in
|
||||
print("changed")
|
||||
}, onCommit: {
|
||||
self.onAppear()
|
||||
})
|
||||
.padding(.horizontal, 10)
|
||||
.foregroundColor(Color.secondary)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
ZStack {
|
||||
VStack {
|
||||
Spacer().frame(height: 6)
|
||||
TextField("Search", text: $viewModel.searchQuery, onEditingChanged: { _ in
|
||||
print("changed")
|
||||
})
|
||||
.padding(.horizontal, 10)
|
||||
.foregroundColor(Color.secondary)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
ScrollView(.vertical) {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(items, id: \.Id) { item in
|
||||
Button() {
|
||||
_linkedItem.wrappedValue = item;
|
||||
_close.wrappedValue = false;
|
||||
_open.wrappedValue = true;
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
if(item.Type == "Movie") {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width:100, height: 150)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width:100, height: 150)
|
||||
.cornerRadius(10).overlay(
|
||||
ZStack {
|
||||
if(item.ItemBadge == 0) {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("\(String(item.ItemBadge ?? 0))")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}.background(Color.black)
|
||||
.opacity(0.8)
|
||||
.cornerRadius(10.0)
|
||||
.padding(3), alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
Text(item.Name)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(String(item.ProductionYear))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}.frame(width: 100)
|
||||
ForEach(viewModel.items, id: \.Id) { item in
|
||||
NavigationLink(destination: ItemView(item: item)) {
|
||||
ResumeItemGridCell(item: item)
|
||||
}
|
||||
}
|
||||
}.onChange(of: isPortrait) { ip in
|
||||
}.onChange(of: isPortrait) { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onAppear(perform: onAppear)
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicator($viewModel.isLoading)
|
||||
} else if viewModel.items.isEmpty {
|
||||
Text("Empty Response")
|
||||
}
|
||||
}
|
||||
.onAppear(perform: onAppear)
|
||||
.navigationBarTitle("Search", displayMode: .inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResumeItemGridCell: View {
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
|
||||
var item: ResumeItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if item.Type == "Movie" {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: item
|
||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||
.BlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: item
|
||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||
.BlurHash,
|
||||
size: CGSize(width: 32, height: 32))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10).overlay(ZStack {
|
||||
if item.ItemBadge == 0 {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("\(String(item.ItemBadge ?? 0))")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}.background(Color.black)
|
||||
.opacity(0.8)
|
||||
.cornerRadius(10.0)
|
||||
.padding(3), alignment: .topTrailing)
|
||||
}
|
||||
Text(item.Name)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(String(item.ProductionYear))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}.frame(width: 100)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,355 +5,178 @@
|
|||
// Created by Aiden Vigue on 5/1/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftyRequest
|
||||
import SwiftyJSON
|
||||
import SDWebImageSwiftUI
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
import SwiftyRequest
|
||||
|
||||
struct LibraryView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@State private var prefill_id: String = "";
|
||||
@State private var library_names: [String: String] = [:]
|
||||
@State private var library_ids: [String] = []
|
||||
@State private var selected_library_id: String = "";
|
||||
@State private var isLoading: Bool = true;
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
@ObservedObject
|
||||
var viewModel: LibraryViewModel
|
||||
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
@State private var filterString: String = "&SortBy=SortName&SortOrder=Ascending";
|
||||
@State private var showFiltersPopover: Bool = false;
|
||||
@State private var showSearchPopover: Bool = false;
|
||||
@State private var extraParam: String = "";
|
||||
@State private var title: String = "";
|
||||
@State private var url: String = "";
|
||||
@State private var closeSearch: Bool = false;
|
||||
|
||||
private var itemsPerPage: Int = 100;
|
||||
|
||||
@State private var firstItemIndex: Int = 0;
|
||||
@State private var lastItemIndex: Int = 0;
|
||||
@State private var totalItemCount: Int = 0;
|
||||
|
||||
init(prefill: String?, names: [String: String], libraries: [String]) {
|
||||
_prefill_id = State(wrappedValue: prefill ?? "")
|
||||
_library_names = State(wrappedValue: names)
|
||||
_library_ids = State(wrappedValue: libraries)
|
||||
@State
|
||||
private var showFiltersPopover: Bool = false
|
||||
@State
|
||||
private var showingSearchView: Bool = false
|
||||
|
||||
private var title: String
|
||||
|
||||
@State
|
||||
private var tracks: [GridItem] = []
|
||||
|
||||
init(viewModel: LibraryViewModel, title: String) {
|
||||
self.viewModel = viewModel
|
||||
self.title = title
|
||||
}
|
||||
|
||||
init(prefill: String?, names: [String: String], libraries: [String], filter: String) {
|
||||
_prefill_id = State(wrappedValue: prefill ?? "")
|
||||
_library_names = State(wrappedValue: names)
|
||||
_library_ids = State(wrappedValue: libraries)
|
||||
_filterString = State(wrappedValue: filter);
|
||||
}
|
||||
|
||||
init(filter: String, extraParams: String, title: String) {
|
||||
_prefill_id = State(wrappedValue: "erwt");
|
||||
_filterString = State(wrappedValue: filter);
|
||||
_extraParam = State(wrappedValue: extraParams);
|
||||
_title = State(wrappedValue: title)
|
||||
}
|
||||
|
||||
init(extraParams: String, title: String) {
|
||||
_prefill_id = State(wrappedValue: "erwt");
|
||||
_extraParam = State(wrappedValue: extraParams);
|
||||
_title = State(wrappedValue: title)
|
||||
}
|
||||
|
||||
@State var items: [ResumeItem] = []
|
||||
|
||||
func listOnAppear() {
|
||||
if(_viewDidLoad.wrappedValue == false) {
|
||||
//print("running VDL")
|
||||
_viewDidLoad.wrappedValue = true;
|
||||
_library_ids.wrappedValue.append("favorites")
|
||||
_library_names.wrappedValue["favorites"] = "Favorites"
|
||||
|
||||
_library_ids.wrappedValue.append("genres")
|
||||
_library_names.wrappedValue["genres"] = "Genres - WIP"
|
||||
}
|
||||
}
|
||||
|
||||
func loadItems() {
|
||||
recalcTracks()
|
||||
_isLoading.wrappedValue = true;
|
||||
if(_extraParam.wrappedValue == "") {
|
||||
_url.wrappedValue = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(lastItemIndex - firstItemIndex)&StartIndex=\(firstItemIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(selected_library_id == "favorites" ? "&Filters=IsFavorite" : "&ParentId=" + selected_library_id)\(filterString)"
|
||||
} else {
|
||||
_url.wrappedValue = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(lastItemIndex - firstItemIndex)&StartIndex=\(firstItemIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(filterString)\(extraParam)"
|
||||
}
|
||||
|
||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + _url.wrappedValue)
|
||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||
request.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let body = response.body
|
||||
do {
|
||||
let json = try JSON(data: body)
|
||||
_totalItemCount.wrappedValue = json["TotalRecordCount"].int ?? 0;
|
||||
for (_,item):(String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
if(itemObj.Type == "Series") {
|
||||
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = nil
|
||||
itemObj.SeasonId = nil
|
||||
itemObj.SeriesId = nil
|
||||
itemObj.SeriesName = nil
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
} else {
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
itemObj.Name = item["Name"].string ?? ""
|
||||
itemObj.Type = item["Type"].string ?? ""
|
||||
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
|
||||
itemObj.SeasonId = item["SeasonId"].string ?? nil
|
||||
itemObj.SeriesId = item["SeriesId"].string ?? nil
|
||||
itemObj.SeriesName = item["SeriesName"].string ?? nil
|
||||
}
|
||||
itemObj.Watched = item["UserData"]["Played"].bool ?? false
|
||||
_items.wrappedValue.append(itemObj)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
break
|
||||
case .failure(let error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
_isLoading.wrappedValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func onAppear() {
|
||||
if(_prefill_id.wrappedValue != "") {
|
||||
_selected_library_id.wrappedValue = _prefill_id.wrappedValue;
|
||||
}
|
||||
if(_items.wrappedValue.count == 0) {
|
||||
_firstItemIndex.wrappedValue = 0;
|
||||
_lastItemIndex.wrappedValue = itemsPerPage;
|
||||
loadItems()
|
||||
}
|
||||
guard viewModel.globalData != globalData else { return }
|
||||
recalcTracks()
|
||||
viewModel.globalData = globalData
|
||||
}
|
||||
|
||||
func nextPage() {
|
||||
_firstItemIndex.wrappedValue = _lastItemIndex.wrappedValue;
|
||||
_lastItemIndex.wrappedValue = _firstItemIndex.wrappedValue + itemsPerPage;
|
||||
|
||||
if(_lastItemIndex.wrappedValue > _totalItemCount.wrappedValue) {
|
||||
_firstItemIndex.wrappedValue = _totalItemCount.wrappedValue - itemsPerPage;
|
||||
_lastItemIndex.wrappedValue = _totalItemCount.wrappedValue;
|
||||
}
|
||||
|
||||
_items.wrappedValue = [];
|
||||
loadItems()
|
||||
}
|
||||
|
||||
func previousPage() {
|
||||
_lastItemIndex.wrappedValue = _firstItemIndex.wrappedValue;
|
||||
_firstItemIndex.wrappedValue = _lastItemIndex.wrappedValue - itemsPerPage;
|
||||
|
||||
if(_firstItemIndex.wrappedValue < 0) {
|
||||
_firstItemIndex.wrappedValue = 0;
|
||||
_lastItemIndex.wrappedValue = itemsPerPage;
|
||||
}
|
||||
|
||||
_items.wrappedValue = [];
|
||||
loadItems()
|
||||
}
|
||||
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
||||
@Environment(\.verticalSizeClass)
|
||||
var verticalSizeClass: UserInterfaceSizeClass?
|
||||
@Environment(\.horizontalSizeClass)
|
||||
var horizontalSizeClass: UserInterfaceSizeClass?
|
||||
|
||||
var isPortrait: Bool {
|
||||
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
func recalcTracks() {
|
||||
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125));
|
||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||
_tracks.wrappedValue = []
|
||||
for _ in (0..<trkCnt)
|
||||
{
|
||||
_tracks.wrappedValue.append(GridItem.init(.flexible()))
|
||||
for _ in 0 ..< trkCnt {
|
||||
_tracks.wrappedValue.append(GridItem(.flexible()))
|
||||
}
|
||||
}
|
||||
|
||||
@State private var tracks: [GridItem] = []
|
||||
|
||||
|
||||
var body: some View {
|
||||
if(prefill_id != "") {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
ScrollView(.vertical) {
|
||||
Spacer().frame(height: 16)
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(items, id: \.Id) { item in
|
||||
NavigationLink(destination: ItemView(item: item )) {
|
||||
VStack(alignment: .leading) {
|
||||
if(item.Type == "Movie") {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width:100, height: 150)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width:100, height: 150)
|
||||
.cornerRadius(10).overlay(
|
||||
ZStack {
|
||||
if(item.ItemBadge == 0) {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("\(String(item.ItemBadge ?? 0))")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}.background(Color.black)
|
||||
.opacity(0.8)
|
||||
.cornerRadius(10.0)
|
||||
.padding(3), alignment: .topTrailing
|
||||
)
|
||||
}
|
||||
Text(item.Name)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(String(item.ProductionYear))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}.frame(width: 100)
|
||||
}
|
||||
ZStack {
|
||||
ScrollView(.vertical) {
|
||||
Spacer().frame(height: 16)
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.items, id: \.Id) { item in
|
||||
NavigationLink(destination: ItemView(item: item)) {
|
||||
ItemGridView(item: item)
|
||||
}
|
||||
}
|
||||
Spacer().frame(height: 16)
|
||||
if(totalItemCount > itemsPerPage) {
|
||||
HStack() {
|
||||
Spacer()
|
||||
if(firstItemIndex != 0) {
|
||||
Button {
|
||||
previousPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left").font(.system(size: 30))
|
||||
}
|
||||
}
|
||||
if(lastItemIndex != totalItemCount) {
|
||||
Button {
|
||||
nextPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right").font(.system(size: 30))
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer().frame(height: 16)
|
||||
}
|
||||
.gesture(
|
||||
DragGesture().onChanged { value in
|
||||
if value.translation.height > 0 {
|
||||
print("Scroll down")
|
||||
} else {
|
||||
print("Scroll up")
|
||||
}
|
||||
}
|
||||
)
|
||||
.onChange(of: isPortrait) { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
Spacer().frame(height: 16)
|
||||
}
|
||||
.overrideViewPreference(.unspecified)
|
||||
.onAppear(perform: onAppear)
|
||||
.onChange(of: filterString) { tag in
|
||||
isLoading = true;
|
||||
items = [];
|
||||
firstItemIndex = 0;
|
||||
lastItemIndex = itemsPerPage;
|
||||
loadItems();
|
||||
.onChange(of: isPortrait) { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
.navigationTitle(extraParam == "" ? (library_names[prefill_id] ?? "Library") : title)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if(totalItemCount > itemsPerPage) {
|
||||
if(firstItemIndex != 0) {
|
||||
Button {
|
||||
previousPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
}
|
||||
}
|
||||
if(lastItemIndex != totalItemCount) {
|
||||
Button {
|
||||
nextPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: LibrarySearchView(url: url, close: $closeSearch), isActive: $closeSearch) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ActivityIndicator($viewModel.isLoading)
|
||||
} else if viewModel.items.isEmpty {
|
||||
Text("Empty Response")
|
||||
}
|
||||
}
|
||||
// .overrideViewPreference(.unspecified)
|
||||
.onAppear(perform: onAppear)
|
||||
.navigationTitle(title)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if !viewModel.isHiddenPreviousButton {
|
||||
Button {
|
||||
showFiltersPopover = true
|
||||
viewModel.requestPreviousPage()
|
||||
} label: {
|
||||
Image(systemName: "line.horizontal.3.decrease")
|
||||
Image(systemName: "chevron.left")
|
||||
}
|
||||
}
|
||||
}.fullScreenCover( isPresented: self.$showFiltersPopover) { LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover).environmentObject(self.globalData) }
|
||||
} else {
|
||||
List(library_ids, id:\.self) { id in
|
||||
if(id != "genres") {
|
||||
NavigationLink(destination: LibraryView(prefill: id, names: library_names, libraries: library_ids)) {
|
||||
Text(library_names[id] ?? "").foregroundColor(Color.primary)
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: LibraryView(prefill: id, names: library_names, libraries: library_ids)) {
|
||||
Text(library_names[id] ?? "").foregroundColor(Color.primary)
|
||||
if !viewModel.isHiddenNextButton {
|
||||
Button {
|
||||
viewModel.requestNextPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
}.onAppear(perform: listOnAppear).navigationTitle("All Media")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
NavigationLink(destination: LibrarySearchView(url: "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=300&StartIndex=0&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(extraParam)", close: $closeSearch), isActive: $closeSearch) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
NavigationLink(destination: LazyView { LibrarySearchView(viewModel: .init(filter: viewModel.filter)) }) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
Button {
|
||||
showFiltersPopover = true
|
||||
} label: {
|
||||
Image(systemName: "line.horizontal.3.decrease")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.sheet(isPresented: self.$showFiltersPopover) {
|
||||
LibraryFilterView(library: viewModel.filter.parentID ?? "", filter: $viewModel.filter)
|
||||
.environmentObject(self.globalData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LibraryView {
|
||||
struct ItemGridView: View {
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
var item: ResumeItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if item.Type == "Movie" {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: item
|
||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||
.BlurHash,
|
||||
size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
} else {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: item
|
||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||
.BlurHash,
|
||||
size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10).overlay(ZStack {
|
||||
if item.ItemBadge == 0 {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("\(String(item.ItemBadge ?? 0))")
|
||||
.font(.caption)
|
||||
.padding(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}.background(Color.black)
|
||||
.opacity(0.8)
|
||||
.cornerRadius(10.0)
|
||||
.padding(3), alignment: .topTrailing)
|
||||
}
|
||||
Text(item.Name)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(String(item.ProductionYear))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}.frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -39,7 +39,7 @@ struct NextUpView: View {
|
|||
let json = try JSON(data: body)
|
||||
for (_,item):(String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
var itemObj = ResumeItem()
|
||||
itemObj.Image = item["SeriesPrimaryImageTag"].string ?? ""
|
||||
itemObj.ImageType = "Primary"
|
||||
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
|
||||
|
|
|
@ -123,7 +123,7 @@ struct SeasonItemView: View {
|
|||
episode.ParentId = episode.SeasonId ?? ""
|
||||
episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0)
|
||||
|
||||
let rI = ResumeItem()
|
||||
var rI = ResumeItem()
|
||||
rI.Name = episode.Name
|
||||
rI.Id = episode.Id
|
||||
rI.IndexNumber = episode.IndexNumber
|
||||
|
|
|
@ -37,7 +37,7 @@ struct SeriesItemView: View {
|
|||
let json = try JSON(data: body)
|
||||
for (_,item):(String, JSON) in json["Items"] {
|
||||
// Do something you want
|
||||
let itemObj = ResumeItem()
|
||||
var itemObj = ResumeItem()
|
||||
itemObj.Type = "Season"
|
||||
itemObj.Id = item["Id"].string ?? ""
|
||||
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// LazyView.swift
|
||||
// JellyfinPlayer
|
||||
//
|
||||
// Created by PangMo5 on 2021/05/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct LazyView<Content: View>: View {
|
||||
var content: () -> Content
|
||||
var body: some View {
|
||||
self.content()
|
||||
}
|
||||
}
|
|
@ -5,34 +5,44 @@
|
|||
// Created by Aiden Vigue on 4/29/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
@ObservedObject
|
||||
var viewModel: SettingsViewModel
|
||||
|
||||
@Binding
|
||||
var close: Bool
|
||||
@Environment(\.managedObjectContext)
|
||||
private var viewContext
|
||||
@EnvironmentObject
|
||||
var globalData: GlobalData
|
||||
@EnvironmentObject
|
||||
var jsi: justSignedIn
|
||||
@State
|
||||
private var username: String = ""
|
||||
@State
|
||||
private var inNetworkStreamBitrate: Int = 40_000_000
|
||||
@State
|
||||
private var outOfNetworkStreamBitrate: Int = 40_000_000
|
||||
@State
|
||||
private var autoSelectSubtitles: Bool = false
|
||||
@State
|
||||
private var autoSelectSubtitlesLangcode: String = "none"
|
||||
|
||||
@Binding var close: Bool;
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
@State private var username: String = "";
|
||||
@State private var inNetworkStreamBitrate: Int = 40000000;
|
||||
@State private var outOfNetworkStreamBitrate: Int = 40000000;
|
||||
@State private var autoSelectSubtitles: Bool = false;
|
||||
@State private var autoSelectSubtitlesLangcode: String = "none";
|
||||
|
||||
func onAppear() {
|
||||
_username.wrappedValue = globalData.user?.username ?? "";
|
||||
_username.wrappedValue = globalData.user?.username ?? ""
|
||||
let defaults = UserDefaults.standard
|
||||
_inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth");
|
||||
_outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth");
|
||||
_autoSelectSubtitles.wrappedValue = defaults.bool(forKey: "AutoSelectSubtitles");
|
||||
_autoSelectSubtitlesLangcode.wrappedValue = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? "";
|
||||
_inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth")
|
||||
_outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth")
|
||||
_autoSelectSubtitles.wrappedValue = defaults.bool(forKey: "AutoSelectSubtitles")
|
||||
_autoSelectSubtitlesLangcode.wrappedValue = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView() {
|
||||
Form() {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Playback settings")) {
|
||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
|
@ -42,7 +52,7 @@ struct SettingsView: View {
|
|||
let defaults = UserDefaults.standard
|
||||
defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth")
|
||||
}
|
||||
|
||||
|
||||
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||
Text(bitrate.name).tag(bitrate.value)
|
||||
|
@ -52,19 +62,17 @@ struct SettingsView: View {
|
|||
defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(header: Text("Accessibility")) {
|
||||
Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in
|
||||
let defaults = UserDefaults.standard
|
||||
defaults.setValue(autoSelectSubtitles, forKey: "AutoSelectSubtitles")
|
||||
})
|
||||
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {
|
||||
|
||||
}
|
||||
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
|
||||
}
|
||||
|
||||
Section() {
|
||||
HStack() {
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||
Spacer()
|
||||
Button {
|
||||
|
@ -76,7 +84,7 @@ struct SettingsView: View {
|
|||
} catch _ as NSError {
|
||||
// TODO: handle the error
|
||||
}
|
||||
|
||||
|
||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||
|
||||
|
@ -85,7 +93,7 @@ struct SettingsView: View {
|
|||
} catch _ as NSError {
|
||||
// TODO: handle the error
|
||||
}
|
||||
|
||||
|
||||
globalData.server = nil
|
||||
globalData.user = nil
|
||||
globalData.authToken = ""
|
||||
|
@ -106,9 +114,7 @@ struct SettingsView: View {
|
|||
Button {
|
||||
close = false
|
||||
} label: {
|
||||
HStack() {
|
||||
Text("Back").font(.callout)
|
||||
}
|
||||
Text("Back").font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue