initial screens Refactoring
remove ContentView add HomeView add MainTabView add SplashView add ConnectToServerViewModel add HomeViewModel add LibraryListViewModel add SplashViewModel add ViewModel
This commit is contained in:
parent
f1138b50f2
commit
94aa3bc4b4
|
@ -42,7 +42,6 @@
|
|||
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
|
||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; };
|
||||
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
||||
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||
|
@ -70,6 +69,15 @@
|
|||
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; };
|
||||
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; };
|
||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; };
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
|
||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; };
|
||||
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; };
|
||||
625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; };
|
||||
6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||
|
@ -177,7 +185,6 @@
|
|||
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
|
||||
5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = "<group>"; };
|
||||
5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
5377CBFD263B596B003A4E83 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
|
@ -206,6 +213,14 @@
|
|||
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
||||
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
||||
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||
625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
||||
625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
|
||||
6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = "<group>"; };
|
||||
6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = "<group>"; };
|
||||
628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -242,6 +257,7 @@
|
|||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
||||
621C638026672A30004216EA /* NukeUI in Frameworks */,
|
||||
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
|
||||
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */,
|
||||
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */,
|
||||
);
|
||||
|
@ -266,6 +282,11 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */,
|
||||
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||
625CB5742678C33500530A6E /* LibraryListViewModel.swift */,
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
|
@ -338,10 +359,10 @@
|
|||
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
625CB56D2678C1C400530A6E /* ViewModels */,
|
||||
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||
5377CBF6263B596A003A4E83 /* ContentView.swift */,
|
||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
||||
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
||||
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */,
|
||||
|
@ -366,6 +387,9 @@
|
|||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
||||
53DE4BD1267098F300739748 /* SearchBarView.swift */,
|
||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||
625CB5672678B6FB00530A6E /* SplashView.swift */,
|
||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
|
||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||
);
|
||||
path = JellyfinPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -406,6 +430,13 @@
|
|||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
625CB56D2678C1C400530A6E /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
628B95252670CABD0091AF3B /* WidgetExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -485,6 +516,7 @@
|
|||
53352570265EA0A0006CCA86 /* Introspect */,
|
||||
621C637F26672A30004216EA /* NukeUI */,
|
||||
53A431BC266B0FF20016769F /* JellyfinAPI */,
|
||||
625CB5792678C4A400530A6E /* ActivityIndicator */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
||||
|
@ -549,6 +581,7 @@
|
|||
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */,
|
||||
53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
||||
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -626,16 +659,20 @@
|
|||
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
|
||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
|
||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
||||
|
@ -643,23 +680,26 @@
|
|||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
||||
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
|
||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||
625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */,
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
||||
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
|
||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
||||
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
|
||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */,
|
||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1065,6 +1105,14 @@
|
|||
version = 0.3.0;
|
||||
};
|
||||
};
|
||||
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/duyquang91/ActivityIndicator";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.1.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -1108,6 +1156,11 @@
|
|||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||
productName = NukeUI;
|
||||
};
|
||||
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
|
||||
productName = ActivityIndicator;
|
||||
};
|
||||
628B95322670CAEA0091AF3B /* NukeUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "ActivityIndicator",
|
||||
"repositoryURL": "https://github.com/duyquang91/ActivityIndicator",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0101a02196f6a67cf26f6434b007d3db6bd07fee",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "AnyCodable",
|
||||
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
|
||||
|
|
|
@ -5,259 +5,46 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import KeychainSwift
|
||||
import JellyfinAPI
|
||||
import KeychainSwift
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
@StateObject
|
||||
var viewModel = ConnectToServerViewModel()
|
||||
|
||||
@State private var uri = ""
|
||||
@State private var isWorking = false
|
||||
@State private var isErrored = false
|
||||
@State private var isDone = false
|
||||
@State private var isSignInErrored = false
|
||||
@State private var isConnected = false
|
||||
@State private var serverName = ""
|
||||
@State private var usernameDisabled: Bool = false
|
||||
@State private var publicUsers: [UserDto] = []
|
||||
@State private var lastPublicUsers: [UserDto] = []
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
@State private var server_id = ""
|
||||
@State private var serverSkipped: Bool = false
|
||||
@State private var serverSkippedAlert: Bool = false
|
||||
@State private var skip_server_bool: Bool = false
|
||||
@State private var skip_server_obj: Server!
|
||||
|
||||
@Binding var rootIsActive: Bool
|
||||
|
||||
private var reauthDeviceID: String = ""
|
||||
private let userUUID = UUID()
|
||||
|
||||
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
|
||||
_rootIsActive = isActive
|
||||
skip_server_bool = skip_server
|
||||
skip_server_obj = skip_server_prefill
|
||||
reauthDeviceID = reauth_deviceId
|
||||
}
|
||||
|
||||
init(isActive: Binding<Bool>) {
|
||||
_rootIsActive = isActive
|
||||
}
|
||||
|
||||
func start() {
|
||||
if skip_server_bool {
|
||||
uri = skip_server_obj.baseURI!
|
||||
|
||||
UserAPI.getPublicUsers()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure:
|
||||
skip_server_bool = false
|
||||
skip_server_obj = Server()
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
publicUsers = response
|
||||
|
||||
serverSkipped = true
|
||||
serverSkippedAlert = true
|
||||
server_id = skip_server_obj.server_id!
|
||||
serverName = skip_server_obj.name!
|
||||
isConnected = true
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func doLogin() {
|
||||
isWorking = true
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var deviceName = UIDevice.current.name
|
||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||
|
||||
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""
|
||||
print(authHeader)
|
||||
|
||||
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
|
||||
|
||||
let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil)
|
||||
|
||||
UserAPI.authenticateUserByName(authenticateUserByName: x)
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure(let error):
|
||||
isWorking = false
|
||||
if let err = error as? ErrorResponse {
|
||||
switch err {
|
||||
case .error(401, _, _, _):
|
||||
isSignInErrored = true
|
||||
case .error:
|
||||
globalData.networkError = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
do {
|
||||
try viewContext.execute(deleteRequest)
|
||||
} catch _ as NSError {
|
||||
|
||||
}
|
||||
|
||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||
|
||||
do {
|
||||
try viewContext.execute(deleteRequest2)
|
||||
} catch _ as NSError {
|
||||
|
||||
}
|
||||
|
||||
let newServer = Server(context: viewContext)
|
||||
newServer.baseURI = uri
|
||||
newServer.name = serverName
|
||||
newServer.server_id = server_id
|
||||
|
||||
let newUser = SignedInUser(context: viewContext)
|
||||
newUser.device_uuid = userUUID.uuidString
|
||||
newUser.username = username
|
||||
newUser.user_id = response.user!.id!
|
||||
|
||||
let keychain = KeychainSwift()
|
||||
keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)")
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
DispatchQueue.main.async { [self] in
|
||||
globalData.authHeader = authHeader
|
||||
_rootIsActive.wrappedValue = false
|
||||
|
||||
globalData.expiredCredentials = false
|
||||
globalData.networkError = false
|
||||
globalData.user = newUser
|
||||
globalData.server = newServer
|
||||
|
||||
jsi.did = true
|
||||
print("logged in")
|
||||
}
|
||||
} catch {
|
||||
print("Couldn't store objects to CoreData")
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
@Binding
|
||||
var isLoggedIn: Bool
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if !isConnected {
|
||||
Section(header: Text("Server Information")) {
|
||||
TextField("Jellyfin Server URL", text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
Button {
|
||||
isWorking = true
|
||||
if !uri.contains("http") {
|
||||
uri = "https://" + uri
|
||||
}
|
||||
if uri.last == "/" {
|
||||
uri = String(uri.dropLast())
|
||||
ZStack {
|
||||
Form {
|
||||
if viewModel.isConnectedServer {
|
||||
if viewModel.publicUsers.isEmpty {
|
||||
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
|
||||
TextField("Username", text: $viewModel.username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
SecureField("Password", text: $viewModel.password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
Button {
|
||||
viewModel.login()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Login")
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}.disabled(viewModel.isLoading || viewModel.username.isEmpty)
|
||||
}
|
||||
|
||||
JellyfinAPI.basePath = uri
|
||||
SystemAPI.getPublicSystemInfo()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure:
|
||||
isErrored = true
|
||||
isWorking = false
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
let server = response
|
||||
serverName = server.serverName!
|
||||
server_id = server.id!
|
||||
if server.startupWizardCompleted ?? true {
|
||||
isConnected = true
|
||||
|
||||
UserAPI.getPublicUsers()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure:
|
||||
isErrored = true
|
||||
isWorking = false
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
publicUsers = response
|
||||
isWorking = false
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
if isWorking == true {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}.disabled(isWorking || uri.isEmpty)
|
||||
}.alert(isPresented: $isErrored) {
|
||||
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
||||
}
|
||||
} else {
|
||||
if publicUsers.count == 0 {
|
||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||
TextField("Username", text: $username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.disabled(usernameDisabled)
|
||||
SecureField("Password", text: $password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
Button {
|
||||
doLogin()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Login")
|
||||
Spacer()
|
||||
if isWorking {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}.disabled(isWorking || username.isEmpty)
|
||||
.alert(isPresented: $isSignInErrored) {
|
||||
Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back")))
|
||||
}
|
||||
}
|
||||
|
||||
if serverSkipped {
|
||||
Section {
|
||||
Button {
|
||||
serverSkippedAlert = false
|
||||
server_id = ""
|
||||
serverName = ""
|
||||
isConnected = false
|
||||
serverSkipped = false
|
||||
viewModel.isConnectedServer = false
|
||||
} label: {
|
||||
HStack {
|
||||
HStack {
|
||||
|
@ -269,85 +56,85 @@ struct ConnectToServerView: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Section {
|
||||
Button {
|
||||
publicUsers = lastPublicUsers
|
||||
usernameDisabled = false
|
||||
} label: {
|
||||
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
|
||||
ForEach(viewModel.publicUsers, id: \.id) { publicUser in
|
||||
HStack {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||
ForEach(publicUsers, id: \.id) { publicUser in
|
||||
HStack {
|
||||
Button() {
|
||||
if publicUser.hasPassword ?? true {
|
||||
lastPublicUsers = publicUsers
|
||||
username = publicUser.name ?? ""
|
||||
usernameDisabled = true
|
||||
publicUsers = []
|
||||
} else {
|
||||
publicUsers = []
|
||||
password = ""
|
||||
username = publicUser.name ?? ""
|
||||
doLogin()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
if publicUser.primaryImageTag != nil {
|
||||
ImageView(src: URL(string: "\(uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(30.0)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||
.font(.system(size: 35))
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color(red: 98/255, green: 121/255, blue: 205/255))
|
||||
.cornerRadius(30.0)
|
||||
.shadow(radius: 6)
|
||||
Button(action: {
|
||||
viewModel.username = publicUser.name ?? ""
|
||||
viewModel.publicUsers.removeAll()
|
||||
if !(publicUser.hasPassword ?? true) {
|
||||
viewModel.password = ""
|
||||
viewModel.login()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
if publicUser.primaryImageTag != nil {
|
||||
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(30.0)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||
.font(.system(size: 35))
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
|
||||
.cornerRadius(30.0)
|
||||
.shadow(radius: 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button() {
|
||||
lastPublicUsers = publicUsers
|
||||
publicUsers = []
|
||||
username = ""
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||
.font(.system(size: 35))
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color(red: 98/255, green: 121/255, blue: 205/255))
|
||||
.cornerRadius(30.0)
|
||||
.shadow(radius: 6)
|
||||
Section {
|
||||
Button {
|
||||
viewModel.publicUsers.removeAll()
|
||||
viewModel.username = ""
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||
.font(.system(size: 35))
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
|
||||
.cornerRadius(30.0)
|
||||
.shadow(radius: 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Section(header: Text("Server Information")) {
|
||||
TextField("Jellyfin Server URL", text: $viewModel.uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
Button {
|
||||
viewModel.connectToServer()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading || viewModel.uri.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.navigationTitle("Connect to Server")
|
||||
.alert(isPresented: $serverSkippedAlert) {
|
||||
Alert(title: Text("Error"), message: Text("Credentials have expired"), dismissButton: .default(Text("Sign in again")))
|
||||
}
|
||||
.onAppear(perform: start)
|
||||
.alert(item: $viewModel.errorMessage) { _ in
|
||||
Alert(title: Text("Error"), message: Text("message"), dismissButton: .default(Text("Try again")))
|
||||
}
|
||||
.onReceive(viewModel.$isLoggedIn, perform: { flag in
|
||||
isLoggedIn = flag
|
||||
})
|
||||
.navigationTitle("Connect to Server")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,231 +0,0 @@
|
|||
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import KeychainSwift
|
||||
import Nuke
|
||||
import JellyfinAPI
|
||||
import WidgetKit
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
|
||||
@State private var needsToSelectServer = 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
|
||||
@State private var loadState: Int = 2
|
||||
|
||||
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
|
||||
var servers: FetchedResults<Server>
|
||||
|
||||
@FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)])
|
||||
var savedUsers: FetchedResults<SignedInUser>
|
||||
|
||||
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
|
||||
|
||||
func startup() {
|
||||
if viewDidLoad == true {
|
||||
return
|
||||
}
|
||||
|
||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||
|
||||
if servers.isEmpty {
|
||||
isLoading = false
|
||||
needsToSelectServer = true
|
||||
} else {
|
||||
isLoading = true
|
||||
let savedUser = savedUsers[0]
|
||||
|
||||
let keychain = KeychainSwift()
|
||||
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
|
||||
if keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil {
|
||||
globalData.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? ""
|
||||
globalData.server = servers[0]
|
||||
globalData.user = savedUser
|
||||
}
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var deviceName = UIDevice.current.name
|
||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||
|
||||
var header = "MediaBrowser "
|
||||
header.append("Client=\"SwiftFin\", ")
|
||||
header.append("Device=\"\(deviceName)\", ")
|
||||
header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ")
|
||||
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
|
||||
header.append("Token=\"\(globalData.authToken)\"")
|
||||
|
||||
globalData.authHeader = header
|
||||
JellyfinAPI.basePath = ServerEnvironment.current.server.baseURI ?? ""
|
||||
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserAPI.getCurrentUser()
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
loadState = loadState - 1
|
||||
}, receiveValue: { response in
|
||||
libraries = response.configuration?.orderedViews ?? []
|
||||
librariesShowRecentlyAdded = libraries.filter { element in
|
||||
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
||||
}
|
||||
|
||||
if loadState == 1 {
|
||||
isLoading = false
|
||||
viewDidLoad = true
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
|
||||
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
loadState = loadState - 1
|
||||
}, receiveValue: { response in
|
||||
response.items?.forEach({ item in
|
||||
library_names[item.id ?? ""] = item.name
|
||||
})
|
||||
|
||||
if loadState == 1 {
|
||||
isLoading = false
|
||||
viewDidLoad = true
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
|
||||
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(40_000_000, forKey: "OutOfNetworkBandwidth")
|
||||
}
|
||||
}
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if needsToSelectServer == true || globalData.user == nil || globalData.server == nil {
|
||||
NavigationView {
|
||||
ConnectToServerView(isActive: $needsToSelectServer)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(globalData)
|
||||
.onAppear(perform: startup)
|
||||
} else if globalData.expiredCredentials == true {
|
||||
NavigationView {
|
||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
||||
reauth_deviceId: globalData.user.device_uuid!, isActive: $globalData.expiredCredentials)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(globalData)
|
||||
.onAppear(perform: startup)
|
||||
} else {
|
||||
if !jsi.did {
|
||||
if isLoading || globalData.user == nil || SessionManager.current.userID == nil {
|
||||
ProgressView()
|
||||
.onAppear(perform: startup)
|
||||
} else {
|
||||
VStack {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
ScrollView {
|
||||
Spacer().frame(height: orientation == .portrait ? 0 : 16)
|
||||
ContinueWatchingView()
|
||||
NextUpView()
|
||||
|
||||
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))
|
||||
Spacer()
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: library_id,
|
||||
title: library_names[library_id] ?? "", usingFilters: recentFilterSet)
|
||||
}) {
|
||||
HStack {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
}
|
||||
}
|
||||
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
LatestMediaView(usingParentID: library_id)
|
||||
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
|
||||
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showSettingsPopover = true
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showSettingsPopover) {
|
||||
SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text("Home")
|
||||
Image(systemName: "house")
|
||||
}
|
||||
.tag("Home")
|
||||
NavigationView {
|
||||
LibraryListView(libraries: library_names)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text("All Media")
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
.tag("All Media")
|
||||
}
|
||||
}
|
||||
.environmentObject(globalData)
|
||||
.onAppear(perform: startup)
|
||||
.alert(isPresented: $globalData.networkError) {
|
||||
Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok")))
|
||||
}
|
||||
.onRotate { orientation in
|
||||
self.orientation = orientation
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Please wait...")
|
||||
.onAppear(perform: {
|
||||
DispatchQueue.main.async { [self] in
|
||||
_viewDidLoad.wrappedValue = false
|
||||
sleep(1)
|
||||
self.jsi.did = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,21 +32,7 @@ struct ProgressBar: Shape {
|
|||
}
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
@State private var items: [BaseItemDto] = []
|
||||
|
||||
func onAppear() {
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
items = response.items ?? []
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
}
|
||||
}
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
@ -97,6 +83,6 @@ struct ContinueWatchingView: View {
|
|||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}.onAppear(perform: onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,11 @@ import JellyfinAPI
|
|||
import Combine
|
||||
|
||||
struct EpisodeItemView: View {
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||
@Environment(\.verticalSizeClass) var vSizeClass
|
||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
||||
|
||||
var item: BaseItemDto
|
||||
|
@ -18,7 +22,6 @@ struct EpisodeItemView: View {
|
|||
@State private var settingState: Bool = true
|
||||
@State private var watched: Bool = false {
|
||||
didSet {
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
if !settingState {
|
||||
if watched == true {
|
||||
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
|
@ -26,14 +29,14 @@ struct EpisodeItemView: View {
|
|||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
} else {
|
||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +45,6 @@ struct EpisodeItemView: View {
|
|||
@State
|
||||
private var favorite: Bool = false {
|
||||
didSet {
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
if !settingState {
|
||||
if favorite == true {
|
||||
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
|
@ -50,14 +52,14 @@ struct EpisodeItemView: View {
|
|||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
} else {
|
||||
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +154,7 @@ struct EpisodeItemView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if orientation == .portrait {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@StateObject
|
||||
var viewModel = HomeViewModel()
|
||||
@State
|
||||
private var orientation = UIDevice.current.orientation
|
||||
@Environment(\.horizontalSizeClass)
|
||||
var hSizeClass
|
||||
@Environment(\.verticalSizeClass)
|
||||
var vSizeClass
|
||||
@State
|
||||
var showingSettings = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Spacer().frame(height: hSizeClass == .compact && vSizeClass == .regular ? 0 : 16)
|
||||
if !viewModel.resumeItems.isEmpty {
|
||||
ContinueWatchingView(items: viewModel.resumeItems)
|
||||
}
|
||||
if !viewModel.nextUpItems.isEmpty {
|
||||
NextUpView(items: viewModel.nextUpItems)
|
||||
}
|
||||
if !viewModel.librariesShowRecentlyAddedIDs.isEmpty {
|
||||
ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in
|
||||
VStack(alignment: .leading) {
|
||||
let library = viewModel.libraries.first(where: { $0.id == libraryID })
|
||||
HStack {
|
||||
Text("Latest \(library?.name ?? "")")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
|
||||
Spacer()
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: libraryID,
|
||||
title: library?.name ?? "", usingFilters: viewModel.recentFilterSet)
|
||||
}) {
|
||||
HStack {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
}
|
||||
}
|
||||
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
LatestMediaView(usingParentID: libraryID)
|
||||
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
||||
}
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onRotate {
|
||||
orientation = $0
|
||||
}
|
||||
.navigationTitle(MainTabView.Tab.home.localized)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showingSettings = true
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingSettings) {
|
||||
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -141,15 +141,13 @@ extension View {
|
|||
@main
|
||||
struct JellyfinPlayerApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
@StateObject private var jsi = justSignedIn()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
SplashView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(jsi)
|
||||
.withHostingWindow { window in
|
||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(jsi))
|
||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import Combine
|
|||
|
||||
struct LatestMediaView: View {
|
||||
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State var items: [BaseItemDto] = []
|
||||
private var library_id: String = ""
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
@ -24,8 +26,6 @@ struct LatestMediaView: View {
|
|||
return
|
||||
}
|
||||
viewDidLoad = true
|
||||
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.userID!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
|
||||
|
@ -34,7 +34,7 @@ struct LatestMediaView: View {
|
|||
}, receiveValue: { response in
|
||||
items = response
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,51 +9,33 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
struct LibraryListView: View {
|
||||
|
||||
@State var library_ids: [String] = ["favorites", "genres"]
|
||||
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"]
|
||||
var libraries: [String: String] = [:] // input libraries
|
||||
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
|
||||
|
||||
init(libraries: [String: String]) {
|
||||
self.libraries = libraries
|
||||
}
|
||||
|
||||
func onAppear() {
|
||||
if library_ids.count == 2 {
|
||||
libraries.forEach { k, v in
|
||||
print("\(k): \(v)")
|
||||
_library_ids.wrappedValue.append(k)
|
||||
_library_names.wrappedValue[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@StateObject
|
||||
var viewModel = LibraryListViewModel()
|
||||
|
||||
var body: some View {
|
||||
List(library_ids, id: \.self) { key in
|
||||
switch key {
|
||||
case "favorites":
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites)
|
||||
}) {
|
||||
Text(library_names[key] ?? "")
|
||||
}
|
||||
case "genres":
|
||||
NavigationLink(destination: LazyView {
|
||||
EmptyView()
|
||||
}) {
|
||||
Text(library_names[key] ?? "")
|
||||
}
|
||||
default:
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: key, title: library_names[key] ?? "")
|
||||
}) {
|
||||
Text(library_names[key] ?? "")
|
||||
}
|
||||
List(viewModel.libraries, id: \.self) { library in
|
||||
switch library.id {
|
||||
case "favorites":
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: "", title: library.name ?? "", usingFilters: viewModel.withFavorites)
|
||||
}) {
|
||||
Text(library.name ?? "")
|
||||
}
|
||||
case "genres":
|
||||
NavigationLink(destination: LazyView {
|
||||
EmptyView()
|
||||
}) {
|
||||
Text(library.name ?? "")
|
||||
}
|
||||
default:
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(usingParentID: library.id ?? "", title: library.name ?? "")
|
||||
}) {
|
||||
Text(library.name ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("All Media")
|
||||
.onAppear(perform: onAppear)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
NavigationLink(destination: LazyView {
|
||||
|
|
|
@ -11,6 +11,8 @@ import Combine
|
|||
|
||||
struct LibrarySearchView: View {
|
||||
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var searchQuery: String = ""
|
||||
@State private var isLoading: Bool = false
|
||||
|
@ -28,7 +30,6 @@ struct LibrarySearchView: View {
|
|||
|
||||
func requestSearch(query: String) {
|
||||
isLoading = true
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
|
||||
.sink(receiveCompletion: { completion in
|
||||
|
@ -37,7 +38,7 @@ struct LibrarySearchView: View {
|
|||
items = response.items ?? []
|
||||
isLoading = false
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import Combine
|
|||
|
||||
struct LibraryView: View {
|
||||
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var isLoading: Bool = false
|
||||
|
||||
|
@ -66,8 +68,6 @@ struct LibraryView: View {
|
|||
|
||||
isLoading = true
|
||||
items = []
|
||||
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
|
||||
|
@ -81,7 +81,7 @@ struct LibraryView: View {
|
|||
isLoading = false
|
||||
viewDidLoad = true
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@State private var tabSelection: Tab = .home
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView {
|
||||
HomeView()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text(Tab.home.localized)
|
||||
Image(systemName: "house")
|
||||
}
|
||||
.tag(Tab.home)
|
||||
NavigationView {
|
||||
LibraryListView()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text(Tab.allMedia.localized)
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
.tag(Tab.allMedia)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabView {
|
||||
|
||||
enum Tab: String {
|
||||
case home
|
||||
case allMedia
|
||||
|
||||
var localized: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "Home"
|
||||
case .allMedia:
|
||||
return "All Media"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,21 +5,29 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct MovieItemView: View {
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State
|
||||
private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass)
|
||||
var hSizeClass
|
||||
@Environment(\.verticalSizeClass)
|
||||
var vSizeClass
|
||||
@EnvironmentObject
|
||||
private var playbackInfo: VideoPlayerItem
|
||||
|
||||
var item: BaseItemDto
|
||||
|
||||
@State private var settingState: Bool = true
|
||||
@State private var watched: Bool = false {
|
||||
@State
|
||||
private var settingState: Bool = true
|
||||
@State
|
||||
private var watched: Bool = false {
|
||||
didSet {
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
if !settingState {
|
||||
if watched == true {
|
||||
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
|
@ -27,14 +35,14 @@ struct MovieItemView: View {
|
|||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
} else {
|
||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,8 +51,6 @@ struct MovieItemView: View {
|
|||
@State
|
||||
private var favorite: Bool = false {
|
||||
didSet {
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
if !settingState {
|
||||
if favorite == true {
|
||||
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
|
@ -52,21 +58,24 @@ struct MovieItemView: View {
|
|||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
} else {
|
||||
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { _ in
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var portraitHeaderView: some View {
|
||||
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
|
||||
ImageView(src: item
|
||||
.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!,
|
||||
maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
|
||||
bh: item.getBackdropImageBlurHash())
|
||||
.opacity(0.4)
|
||||
.blur(radius: 2.0)
|
||||
}
|
||||
|
@ -156,8 +165,11 @@ struct MovieItemView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if orientation == .portrait {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView,
|
||||
overlayAlignment: .bottomLeading,
|
||||
headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds
|
||||
.width * 0.5625) {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
|
||||
|
@ -196,7 +208,9 @@ struct MovieItemView: View {
|
|||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
VStack {
|
||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100),
|
||||
bh: person.getBlurHash())
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
|
@ -235,7 +249,8 @@ struct MovieItemView: View {
|
|||
} else {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
|
||||
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200),
|
||||
bh: item.getBackdropImageBlurHash())
|
||||
.opacity(0.3)
|
||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
||||
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||
|
@ -243,7 +258,8 @@ struct MovieItemView: View {
|
|||
.blur(radius: 4)
|
||||
HStack {
|
||||
VStack {
|
||||
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
|
||||
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120),
|
||||
bh: item.getPrimaryImageBlurHash())
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
Spacer().frame(height: 15)
|
||||
|
@ -369,10 +385,14 @@ struct MovieItemView: View {
|
|||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
VStack {
|
||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
|
||||
ImageView(src: person
|
||||
.getImage(baseURL: ServerEnvironment.current.server.baseURI!,
|
||||
maxWidth: 100),
|
||||
bh: person.getBlurHash())
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular)
|
||||
.lineLimit(1)
|
||||
.frame(width: 100).foregroundColor(Color.primary)
|
||||
if person.role != "" {
|
||||
Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||
|
|
|
@ -11,27 +11,7 @@ import JellyfinAPI
|
|||
|
||||
struct NextUpView: View {
|
||||
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
func onAppear() {
|
||||
if viewDidLoad == true {
|
||||
return
|
||||
}
|
||||
viewDidLoad = true
|
||||
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||
.sink(receiveCompletion: { result in
|
||||
print(result)
|
||||
}, receiveValue: { response in
|
||||
items = response.items ?? []
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
}
|
||||
}
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -69,7 +49,6 @@ struct NextUpView: View {
|
|||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
.onAppear(perform: onAppear)
|
||||
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,15 @@ import Combine
|
|||
import JellyfinAPI
|
||||
|
||||
struct SeasonItemView: View {
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
@State private var orientation = UIDeviceOrientation.unknown
|
||||
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||
@Environment(\.verticalSizeClass) var vSizeClass
|
||||
|
||||
var item: BaseItemDto = BaseItemDto()
|
||||
@State private var episodes: [BaseItemDto] = []
|
||||
|
||||
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
@ -26,7 +31,6 @@ struct SeasonItemView: View {
|
|||
if viewDidLoad {
|
||||
return
|
||||
}
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.userID!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
|
||||
|
@ -37,7 +41,7 @@ struct SeasonItemView: View {
|
|||
viewDidLoad = true
|
||||
episodes = response.items ?? []
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +80,7 @@ struct SeasonItemView: View {
|
|||
|
||||
@ViewBuilder
|
||||
var innerBody: some View {
|
||||
if orientation == .portrait {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView,
|
||||
staticOverlayView: portraitHeaderOverlayView,
|
||||
overlayAlignment: .bottomLeading,
|
||||
|
|
|
@ -10,6 +10,8 @@ import JellyfinAPI
|
|||
import Combine
|
||||
|
||||
struct SeriesItemView: View {
|
||||
@StateObject
|
||||
var tempViewModel = ViewModel()
|
||||
|
||||
var item: BaseItemDto
|
||||
|
||||
|
@ -25,7 +27,6 @@ struct SeriesItemView: View {
|
|||
|
||||
isLoading = true
|
||||
|
||||
var tempCancellables = Set<AnyCancellable>()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||
|
@ -36,7 +37,7 @@ struct SeriesItemView: View {
|
|||
viewDidLoad = true
|
||||
seasons = response.items ?? []
|
||||
})
|
||||
.store(in: &tempCancellables)
|
||||
.store(in: &tempViewModel.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,18 +10,16 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
|
||||
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
|
||||
@Binding var close: Bool
|
||||
@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"
|
||||
@State private var username: String = ""
|
||||
|
||||
|
||||
func onAppear() {
|
||||
let defaults = UserDefaults.standard
|
||||
username = SessionManager.current.user.username!
|
||||
|
@ -30,7 +28,7 @@ struct SettingsView: View {
|
|||
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")
|
||||
autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
|
@ -43,7 +41,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)
|
||||
|
@ -53,7 +51,7 @@ 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
|
||||
|
@ -61,7 +59,7 @@ struct SettingsView: View {
|
|||
})
|
||||
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||
|
@ -69,27 +67,28 @@ struct SettingsView: View {
|
|||
Button {
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
|
||||
do {
|
||||
try viewContext.execute(deleteRequest)
|
||||
} catch _ as NSError {
|
||||
// TODO: handle the error
|
||||
}
|
||||
|
||||
|
||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||
|
||||
|
||||
do {
|
||||
try viewContext.execute(deleteRequest2)
|
||||
} catch _ as NSError {
|
||||
// TODO: handle the error
|
||||
}
|
||||
|
||||
globalData.server = Server()
|
||||
globalData.user = SignedInUser()
|
||||
globalData.authToken = ""
|
||||
globalData.authHeader = ""
|
||||
jsi.did = true
|
||||
|
||||
do {
|
||||
try SessionManager.current.logout()
|
||||
try ServerEnvironment.current.reset()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
// TODO: This should redirect to the server selection screen
|
||||
exit(-1)
|
||||
} label: {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplashView: View {
|
||||
@StateObject
|
||||
var viewModel = SplashViewModel()
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoggedIn {
|
||||
MainTabView()
|
||||
} else {
|
||||
NavigationView {
|
||||
ConnectToServerView(isLoggedIn: $viewModel.isLoggedIn)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ final class SessionManager {
|
|||
fileprivate(set) var authToken: String!
|
||||
fileprivate(set) var deviceID: String
|
||||
var userID: String? {
|
||||
user.user_id
|
||||
user?.user_id
|
||||
}
|
||||
|
||||
init() {
|
||||
|
|
|
@ -22,7 +22,3 @@ public enum SortBy: String, Codable, CaseIterable {
|
|||
case name = "SortName"
|
||||
case dateAdded = "DateCreated"
|
||||
}
|
||||
|
||||
class justSignedIn: ObservableObject {
|
||||
@Published var did: Bool = false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
final class ConnectToServerViewModel: ViewModel {
|
||||
@Published
|
||||
var publicUsers = [UserDto]()
|
||||
@Published
|
||||
var isConnectedServer = false
|
||||
@Published
|
||||
var isLoggedIn = false
|
||||
@Published
|
||||
var uri = ""
|
||||
@Published
|
||||
var username = ""
|
||||
@Published
|
||||
var password = ""
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
if ServerEnvironment.current.server != nil {
|
||||
UserAPI.getPublicUsers()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure:
|
||||
self.isConnectedServer = false
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
self.publicUsers = response
|
||||
self.isConnectedServer = true
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
func connectToServer() {
|
||||
ServerEnvironment.current.setUp(with: uri)
|
||||
.sink(receiveCompletion: { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.errorMessage = error.localizedDescription
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
guard response.server_id != nil else {
|
||||
return
|
||||
}
|
||||
self.isConnectedServer = true
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func login() {
|
||||
SessionManager.current.login(username: username, password: password)
|
||||
.sink(receiveCompletion: { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.errorMessage = error.localizedDescription
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
guard response.user_id != nil else {
|
||||
return
|
||||
}
|
||||
self.isLoggedIn = true
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import ActivityIndicator
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
final class HomeViewModel: ViewModel {
|
||||
|
||||
@Published
|
||||
var librariesShowRecentlyAddedIDs = [String]()
|
||||
@Published
|
||||
var libraries = [BaseItemDto]()
|
||||
@Published
|
||||
var resumeItems = [BaseItemDto]()
|
||||
@Published
|
||||
var nextUpItems = [BaseItemDto]()
|
||||
|
||||
// temp
|
||||
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
UserAPI.getCurrentUser()
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
let libraries = response.configuration?.orderedViews ?? []
|
||||
self.librariesShowRecentlyAddedIDs = libraries.filter { element in
|
||||
!(response.configuration?.latestItemsExcludes?.contains(element))!
|
||||
}
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
self.libraries = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
self.resumeItems = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12,
|
||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { result in
|
||||
print(result)
|
||||
}, receiveValue: { response in
|
||||
self.nextUpItems = response.items ?? []
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
final class LibraryListViewModel: ViewModel {
|
||||
@Published
|
||||
var libraries = [BaseItemDto]()
|
||||
|
||||
// temp
|
||||
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
libraries.append(.init(name: "Favorites", id: "favorites"))
|
||||
libraries.append(.init(name: "Genres", id: "genres"))
|
||||
refresh()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
self.libraries.append(contentsOf: response.items ?? [])
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Nuke
|
||||
import WidgetKit
|
||||
|
||||
final class SplashViewModel: ViewModel {
|
||||
|
||||
@Published
|
||||
var isLoggedIn: Bool
|
||||
|
||||
override init() {
|
||||
isLoggedIn = ServerEnvironment.current.server != nil && SessionManager.current.user != nil
|
||||
super.init()
|
||||
|
||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
|
||||
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(40_000_000, forKey: "OutOfNetworkBandwidth")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import ActivityIndicator
|
||||
|
||||
typealias ErrorMessage = String
|
||||
|
||||
extension ErrorMessage: Identifiable {
|
||||
public var id: String {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
class ViewModel: ObservableObject {
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
@Published
|
||||
var isLoading = true
|
||||
let loading = ActivityIndicator()
|
||||
@Published
|
||||
var errorMessage: ErrorMessage?
|
||||
|
||||
init() {
|
||||
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue