Add server page

This commit is contained in:
Shav Kinderlehrer 2023-12-12 01:22:43 -05:00
parent 8a2b75dbfa
commit 5ed7d2551c
12 changed files with 374 additions and 9 deletions

11
Info.plist Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
3D1015D92B27F57400F5C29A /* AddServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015D82B27F57400F5C29A /* AddServerView.swift */; };
3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */; };
3D1015DE2B27F79900F5C29A /* DatamodelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */; };
3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015E32B28000E00F5C29A /* AuthStateController.swift */; };
3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CA2B279A310063DD2A /* JelApp.swift */; };
3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CC2B279A310063DD2A /* ContentView.swift */; };
3D9063CF2B279A320063DD2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D9063CE2B279A320063DD2A /* Assets.xcassets */; };
@ -14,6 +18,8 @@
3D9063DD2B279A320063DD2A /* JelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063DC2B279A320063DD2A /* JelTests.swift */; };
3D9063E72B279A320063DD2A /* JelUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E62B279A320063DD2A /* JelUITests.swift */; };
3D9063E92B279A320063DD2A /* JelUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */; };
3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D9064582B27E4C70063DD2A /* JellyfinKit */; };
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -33,7 +39,25 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
3D9064142B279A450063DD2A /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
3D1015D42B27F49000F5C29A /* JellyfinKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = JellyfinKit; path = ../JellyfinKit; sourceTree = "<group>"; };
3D1015D82B27F57400F5C29A /* AddServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerView.swift; sourceTree = "<group>"; };
3D1015DB2B27F5D300F5C29A /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
3D1015DD2B27F79900F5C29A /* DatamodelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatamodelController.swift; sourceTree = "<group>"; };
3D1015E32B28000E00F5C29A /* AuthStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthStateController.swift; sourceTree = "<group>"; };
3D9063C72B279A310063DD2A /* Jel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jel.app; sourceTree = BUILT_PRODUCTS_DIR; };
3D9063CA2B279A310063DD2A /* JelApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelApp.swift; sourceTree = "<group>"; };
3D9063CC2B279A310063DD2A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -45,6 +69,8 @@
3D9063E22B279A320063DD2A /* JelUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JelUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3D9063E62B279A320063DD2A /* JelUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITests.swift; sourceTree = "<group>"; };
3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITestsLaunchTests.swift; sourceTree = "<group>"; };
3DC0E5802B2832B9001CCE96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinClientController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -52,6 +78,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -72,12 +99,39 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3D1015D72B27F54A00F5C29A /* Views */ = {
isa = PBXGroup;
children = (
3D1015D82B27F57400F5C29A /* AddServerView.swift */,
);
path = Views;
sourceTree = "<group>";
};
3D1015DF2B27F8EE00F5C29A /* Controllers */ = {
isa = PBXGroup;
children = (
3D1015DD2B27F79900F5C29A /* DatamodelController.swift */,
3D1015E32B28000E00F5C29A /* AuthStateController.swift */,
3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
3D1015E02B27FE5700F5C29A /* Models */ = {
isa = PBXGroup;
children = (
);
path = Models;
sourceTree = "<group>";
};
3D9063BE2B279A310063DD2A = {
isa = PBXGroup;
children = (
3DC0E5802B2832B9001CCE96 /* Info.plist */,
3D9063C92B279A310063DD2A /* Jel */,
3D9063DB2B279A320063DD2A /* JelTests */,
3D9063E52B279A320063DD2A /* JelUITests */,
3D1015D42B27F49000F5C29A /* JellyfinKit */,
3D9063C82B279A310063DD2A /* Products */,
);
sourceTree = "<group>";
@ -95,11 +149,15 @@
3D9063C92B279A310063DD2A /* Jel */ = {
isa = PBXGroup;
children = (
3D1015E02B27FE5700F5C29A /* Models */,
3D1015DF2B27F8EE00F5C29A /* Controllers */,
3D1015D72B27F54A00F5C29A /* Views */,
3D9063CA2B279A310063DD2A /* JelApp.swift */,
3D9063CC2B279A310063DD2A /* ContentView.swift */,
3D9063CE2B279A320063DD2A /* Assets.xcassets */,
3D9063D02B279A320063DD2A /* Jel.entitlements */,
3D9063D12B279A320063DD2A /* Preview Content */,
3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */,
);
path = Jel;
sourceTree = "<group>";
@ -139,12 +197,16 @@
3D9063C32B279A310063DD2A /* Sources */,
3D9063C42B279A310063DD2A /* Frameworks */,
3D9063C52B279A310063DD2A /* Resources */,
3D9064142B279A450063DD2A /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Jel;
packageProductDependencies = (
3D9064582B27E4C70063DD2A /* JellyfinKit */,
);
productName = Jel;
productReference = 3D9063C72B279A310063DD2A /* Jel.app */;
productType = "com.apple.product-type.application";
@ -217,6 +279,8 @@
Base,
);
mainGroup = 3D9063BE2B279A310063DD2A;
packageReferences = (
);
productRefGroup = 3D9063C82B279A310063DD2A /* Products */;
projectDirPath = "";
projectRoot = "";
@ -259,8 +323,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3D1015DE2B27F79900F5C29A /* DatamodelController.swift in Sources */,
3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */,
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */,
3D1015D92B27F57400F5C29A /* AddServerView.swift in Sources */,
3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */,
3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */,
3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -353,6 +422,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@ -407,6 +477,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
@ -414,6 +485,7 @@
3D9063ED2B279A320063DD2A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Jel/Jel.entitlements;
@ -424,6 +496,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@ -452,6 +525,7 @@
3D9063EE2B279A320063DD2A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Jel/Jel.entitlements;
@ -462,6 +536,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@ -617,6 +692,26 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
3D9064582B27E4C70063DD2A /* JellyfinKit */ = {
isa = XCSwiftPackageProductDependency;
productName = JellyfinKit;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
3D1015DB2B27F5D300F5C29A /* Model.xcdatamodel */,
);
currentVersion = 3D1015DB2B27F5D300F5C29A /* Model.xcdatamodel */;
path = Model.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = 3D9063BF2B279A310063DD2A /* Project object */;
}

View File

@ -0,0 +1,25 @@
{
"object": {
"pins": [
{
"package": "Get",
"repositoryURL": "https://github.com/kean/Get",
"state": {
"branch": null,
"revision": "12830cc64f31789ae6f4352d2d51d03a25fc3741",
"version": "2.1.6"
}
},
{
"package": "URLQueryEncoder",
"repositoryURL": "https://github.com/CreateAPI/URLQueryEncoder",
"state": {
"branch": null,
"revision": "4ce950479707ea109f229d7230ec074a133b15d7",
"version": "0.2.1"
}
}
]
},
"version": 1
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "26628D67-145A-45DA-9022-10BF196E0789"
type = "1"
version = "2.0">
</Bucket>

View File

@ -9,6 +9,16 @@
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>JellyfinClient.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>JellyfinKit.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -8,17 +8,24 @@
import SwiftUI
struct ContentView: View {
@ObservedObject var authState: AuthStateController
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
if !authState.loggedIn {
AddServerView(authState: authState)
} else {
Text("Logged in")
Button("Log out") {
authState.loggedIn = false
authState.save()
}
}
}
.padding()
}
}
#Preview {
ContentView()
ContentView(authState: AuthStateController())
}

View File

@ -0,0 +1,38 @@
//
// AuthStateController.swift
// Jel
//
// Created by zerocool on 12/11/23.
//
import Foundation
class AuthStateController: ObservableObject {
@Published var loggedIn: Bool
@Published var serverUrl: URL?
@Published var authToken: String?
private let defaults = UserDefaults.standard
init(loggedIn: Bool = false, serverUrl: URL? = nil, authToken: String? = nil) {
self.loggedIn = loggedIn
self.serverUrl = serverUrl
self.authToken = authToken
}
func load() {
self.loggedIn = defaults.bool(forKey: "AuthState_loggedIn")
if let oldServerUrl = defaults.url(forKey: "AuthState_serverUrl") {
self.serverUrl = oldServerUrl
}
if let oldAuthToken = defaults.string(forKey: "AuthState_authToken") {
self.authToken = oldAuthToken
}
}
func save() {
defaults.set(self.loggedIn, forKey: "AuthState_loggedIn")
defaults.set(self.serverUrl, forKey: "AuthState_serverUrl")
defaults.set(self.authToken, forKey: "AuthState_authToken")
}
}

View File

@ -0,0 +1,25 @@
//
// DatamodelController.swift
// Jel
//
// Created by zerocool on 12/11/23.
//
import CoreData
struct DatamodelController {
static let shared = DatamodelController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "Model")
container.loadPersistentStores(completionHandler: {(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}

View File

@ -0,0 +1,30 @@
//
// JellyfinClientController.swift
// Jel
//
// Created by zerocool on 12/12/23.
//
import Foundation
import Get
import JellyfinKit
class JellyfinClientController {
let api: APIClient
init(serverUrl: URL) {
self.api = APIClient(
baseURL: serverUrl
)
}
func isJellyfinServer() async -> Bool {
let request = Paths.getPublicUsers
do {
try await api.send(request)
} catch {
return false
}
return true
}
}

View File

@ -9,9 +9,16 @@ import SwiftUI
@main
struct JelApp: App {
let datamodelController = DatamodelController.shared
let authStateController = AuthStateController()
var body: some Scene {
WindowGroup {
ContentView()
ContentView(authState: authStateController)
.environment(\.managedObjectContext,
datamodelController.container.viewContext)
.task {
authStateController.load()
}
}
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier=""/>

View File

@ -0,0 +1,109 @@
//
// AddServerView.swift
// Jel
//
// Created by zerocool on 12/11/23.
//
import SwiftUI
struct AddServerView: View {
@ObservedObject var authState: AuthStateController
@State var serverUrlString: String = ""
@State var urlHasError: Bool = false
@State var currentErrorMessage: String = ""
@State var loading: Bool = false
@FocusState var serverUrlIsFocused: Bool
var body: some View {
VStack {
Text("Connect to a server")
.font(.title)
HStack {
TextField(text: $serverUrlString) {
Text("http://jellyfin.example.com")
.foregroundStyle(.placeholder)
}
.keyboardType(.URL)
.textContentType(.URL)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($serverUrlIsFocused)
.onChange(of: serverUrlIsFocused) {
if serverUrlIsFocused {
urlHasError = false
}
}
.onSubmit {
Task {
await checkServerUrl()
}
}
if !loading {
Button(action: {
Task {
await checkServerUrl()
}
}) {
Label("Connect", systemImage: "arrow.right")
.labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
.disabled(urlHasError)
} else {
ProgressView()
.progressViewStyle(.circular)
.padding()
}
}
.padding()
if urlHasError {
Text(currentErrorMessage)
.font(.callout)
.foregroundStyle(.red)
}
}
}
func checkServerUrl() async {
loading = true
serverUrlIsFocused = false
if isValidUrl(data: serverUrlString) {
let url = URL(string: serverUrlString)!
if await JellyfinClientController(serverUrl: url).isJellyfinServer() {
authState.serverUrl = url
urlHasError = false
} else {
urlHasError = true
currentErrorMessage = "Server not responding"
}
} else {
urlHasError = true
currentErrorMessage = "Invalid url"
}
loading = false
}
func isValidUrl(data: String) -> Bool {
if let url = URL(string: data) {
if UIApplication.shared.canOpenURL(url) {
return true
}
}
return false
}
}
#Preview {
AddServerView(authState: AuthStateController())
}