diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj index c40e64e..47f32b2 100644 --- a/Jel.xcodeproj/project.pbxproj +++ b/Jel.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 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 */; }; + 3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */; }; 3D7709392B29139700199889 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7709382B29139700199889 /* Pulse */; }; 3D77093B2B29139700199889 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3D77093A2B29139700199889 /* PulseUI */; }; 3D77093D2B29350700199889 /* ConsoleSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D77093C2B29350700199889 /* ConsoleSheetView.swift */; }; @@ -25,6 +26,9 @@ 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDC82B28C62800919017 /* SignInView.swift */; }; 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */; }; 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */; }; + 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC6BA2C2B2A422300416B9F /* SettingsController.swift */; }; + 3DDD67932B293BC40026781E /* DashBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDD67922B293BC40026781E /* DashBoardView.swift */; }; + 3DDD67962B29E28B0026781E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDD67952B29E28B0026781E /* SettingsView.swift */; }; 3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */; }; /* End PBXBuildFile section */ @@ -64,6 +68,7 @@ 3D1015DB2B27F5D300F5C29A /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatamodelController.swift; sourceTree = ""; }; 3D1015E32B28000E00F5C29A /* AuthStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthStateController.swift; sourceTree = ""; }; + 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = ""; }; 3D77093C2B29350700199889 /* ConsoleSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleSheetView.swift; sourceTree = ""; }; 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 = ""; }; @@ -80,6 +85,9 @@ 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInToServerView.swift; sourceTree = ""; }; 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinDateFormatter.swift; sourceTree = ""; }; 3DC0E5802B2832B9001CCE96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3DC6BA2C2B2A422300416B9F /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; + 3DDD67922B293BC40026781E /* DashBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashBoardView.swift; sourceTree = ""; }; + 3DDD67952B29E28B0026781E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinClientController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -115,8 +123,10 @@ isa = PBXGroup; children = ( 3D9063CC2B279A310063DD2A /* ContentView.swift */, + 3DDD67902B293B780026781E /* Dashboard */, + 3DDD67942B29E27A0026781E /* Settings */, 3D91FDC52B28C28900919017 /* SignIn */, - 3D77093C2B29350700199889 /* ConsoleSheetView.swift */, + 3DDD67912B293B9E0026781E /* Utility */, ); path = Views; sourceTree = ""; @@ -127,6 +137,7 @@ 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */, 3D1015E32B28000E00F5C29A /* AuthStateController.swift */, 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */, + 3DC6BA2C2B2A422300416B9F /* SettingsController.swift */, ); path = Controllers; sourceTree = ""; @@ -211,6 +222,31 @@ path = SignIn; sourceTree = ""; }; + 3DDD67902B293B780026781E /* Dashboard */ = { + isa = PBXGroup; + children = ( + 3DDD67922B293BC40026781E /* DashBoardView.swift */, + ); + path = Dashboard; + sourceTree = ""; + }; + 3DDD67912B293B9E0026781E /* Utility */ = { + isa = PBXGroup; + children = ( + 3D77093C2B29350700199889 /* ConsoleSheetView.swift */, + ); + path = Utility; + sourceTree = ""; + }; + 3DDD67942B29E27A0026781E /* Settings */ = { + isa = PBXGroup; + children = ( + 3DDD67952B29E28B0026781E /* SettingsView.swift */, + 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */, + ); + path = Settings; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -359,8 +395,12 @@ 3D77093D2B29350700199889 /* ConsoleSheetView.swift in Sources */, 3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */, 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */, + 3DDD67932B293BC40026781E /* DashBoardView.swift in Sources */, + 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */, 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */, 3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */, + 3DDD67962B29E28B0026781E /* SettingsView.swift in Sources */, + 3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -546,7 +586,7 @@ PRODUCT_BUNDLE_IDENTIFIER = icu.trinket.apps.Jel; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -586,7 +626,7 @@ PRODUCT_BUNDLE_IDENTIFIER = icu.trinket.apps.Jel; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Jel/Controllers/JellyfinClientController.swift b/Jel/Controllers/JellyfinClientController.swift index 24d4615..424c044 100644 --- a/Jel/Controllers/JellyfinClientController.swift +++ b/Jel/Controllers/JellyfinClientController.swift @@ -62,24 +62,27 @@ class JellyfinClientController: ObservableObject { }) } - func isJellyfinServer() async -> Bool { + func getPublicServerInfo() async -> PublicSystemInfo? { let request = Paths.getPublicSystemInfo do { let res = try await api.send(request) if res.statusCode != 200 { throw JellyfinClientError.badResponseCode } + + return res.value } catch { - return false + return nil } - return true } func signIn(username: String, pw: String) async throws { let request = Paths.authenticateUserByName(AuthenticateUserByName(pw: pw, username: username)) let res = try await self.api.send(request) - self.authState.loggedIn = true - self.authState.authToken = res.value.accessToken - self.authState.save() + DispatchQueue.main.async { + self.authState.loggedIn = true + self.authState.authToken = res.value.accessToken + self.authState.save() + } } } diff --git a/Jel/Controllers/SettingsController.swift b/Jel/Controllers/SettingsController.swift new file mode 100644 index 0000000..2b912c4 --- /dev/null +++ b/Jel/Controllers/SettingsController.swift @@ -0,0 +1,44 @@ +// +// SettingsController.swift +// Jel +// +// Created by zerocool on 12/13/23. +// + +import Foundation + +enum AppearanceState: Int, CaseIterable, Identifiable, CustomStringConvertible { + case automatic + case light + case dark + + var id: Self { self } + + var description: String { + switch self { + case .light: + return "Light" + case .dark: + return "Dark" + case .automatic: + return "Automatic" + } + } +} + +class SettingsController: ObservableObject { + static let shared: SettingsController = SettingsController() + + @Published var appearance: AppearanceState = .automatic + + private let defaults = UserDefaults.standard + + func save() { + defaults.set(self.appearance.rawValue, forKey: "Settings_appearance") + } + + func load() { + let oldAppearance = defaults.integer(forKey: "Settings_appearance") + self.appearance = AppearanceState(rawValue: oldAppearance) ?? .automatic + } +} diff --git a/Jel/JelApp.swift b/Jel/JelApp.swift index 6e4490e..3040b5c 100644 --- a/Jel/JelApp.swift +++ b/Jel/JelApp.swift @@ -10,8 +10,6 @@ import SwiftUI @main struct JelApp: App { let datamodelController = DatamodelController.shared - let authStateController = AuthStateController.shared - let jellyfinClientController = JellyfinClientController(authHeaders: AuthHeaders( Client: "Jel", Device: UIDevice.current.systemName, @@ -21,15 +19,13 @@ struct JelApp: App { var body: some Scene { WindowGroup { - ContentView(authState: authStateController) + ContentView() .environment(\.managedObjectContext, datamodelController.container.viewContext) .environmentObject(jellyfinClientController) .task { - authStateController.load() - if authStateController.serverUrl != nil { - jellyfinClientController.setUrl(url: authStateController.serverUrl!) - } + AuthStateController.shared.load() + SettingsController.shared.load() } } } diff --git a/Jel/Views/ConsoleSheetView.swift b/Jel/Views/ConsoleSheetView.swift deleted file mode 100644 index 4d9ba32..0000000 --- a/Jel/Views/ConsoleSheetView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ConsoleSheetView.swift -// Jel -// -// Created by zerocool on 12/12/23. -// - -import SwiftUI -import PulseUI - -struct ConsoleSheetView: View { - @Binding var showingConsoleSheet: Bool - - var body: some View { - NavigationStack { - List { - NavigationLink { - ConsoleView(mode: .network) - .closeButtonHidden() - } label: { - Text("Network Console") - } - NavigationLink { - ConsoleView(mode: .logs) - .closeButtonHidden() - } label: { - Text("Logs Console") - } - } - .toolbar { - Button { - showingConsoleSheet.toggle() - } label: { - Text("Done") - } - } - } - } -} - -#Preview { - ConsoleSheetView(showingConsoleSheet: .constant(true)) -} diff --git a/Jel/Views/ContentView.swift b/Jel/Views/ContentView.swift index 91d5c7c..11a8277 100644 --- a/Jel/Views/ContentView.swift +++ b/Jel/Views/ContentView.swift @@ -9,34 +9,36 @@ import SwiftUI import PulseUI struct ContentView: View { - @ObservedObject var authState: AuthStateController + @EnvironmentObject var jellyfinClient: JellyfinClientController + @StateObject var settingsController: SettingsController = SettingsController.shared + + @StateObject var authState: AuthStateController = AuthStateController.shared @State var showingConsoleSheet: Bool = false var body: some View { VStack { - Button { - showingConsoleSheet.toggle() - } label: { - Label("Console", systemImage: "network") - } - .sheet(isPresented: $showingConsoleSheet) { - ConsoleSheetView(showingConsoleSheet: $showingConsoleSheet) - } - if !authState.loggedIn { - SignInView(authState: authState) + SignInView() } else { - Text("Logged in") - Button("Log out") { - authState.loggedIn = false - authState.save() + NavigationStack { + DashBoardView() } } } .padding() + .preferredColorScheme({ + switch settingsController.appearance { + case .dark: + return ColorScheme.dark + case .light: + return ColorScheme.light + case .automatic: + return .none + } + }()) } } #Preview { - ContentView(authState: AuthStateController()) + ContentView() } diff --git a/Jel/Views/Dashboard/DashBoardView.swift b/Jel/Views/Dashboard/DashBoardView.swift new file mode 100644 index 0000000..ea2003c --- /dev/null +++ b/Jel/Views/Dashboard/DashBoardView.swift @@ -0,0 +1,36 @@ +// +// DashBoardView.swift +// Jel +// +// Created by zerocool on 12/12/23. +// + +import SwiftUI + +struct DashBoardView: View { + @State var showingSettingsSheet: Bool = false + + var body: some View { + NavigationStack { + VStack { + + } + .toolbar { + ToolbarItem { + Button { + showingSettingsSheet.toggle() + } label: { + Label("Settings", systemImage: "gear") + } + } + } + .sheet(isPresented: $showingSettingsSheet) { + SettingsView(showingSettingsView: $showingSettingsSheet) + } + } + } +} + +#Preview { + DashBoardView() +} diff --git a/Jel/Views/Settings/AppearancePicker.swift b/Jel/Views/Settings/AppearancePicker.swift new file mode 100644 index 0000000..93edead --- /dev/null +++ b/Jel/Views/Settings/AppearancePicker.swift @@ -0,0 +1,25 @@ +// +// AppearancePicker.swift +// Jel +// +// Created by zerocool on 12/15/23. +// + +import SwiftUI + +struct AppearancePicker: View { + @ObservedObject var settingsController: SettingsController = SettingsController.shared + + var body: some View { + Picker("Appearance", selection: $settingsController.appearance) { + ForEach(AppearanceState.allCases) { option in + Text(String(describing: option)) + } + } + .pickerStyle(.menu) + } +} + +#Preview { + AppearancePicker() +} diff --git a/Jel/Views/Settings/SettingsView.swift b/Jel/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..6eaa2e2 --- /dev/null +++ b/Jel/Views/Settings/SettingsView.swift @@ -0,0 +1,68 @@ +// +// SettingsView.swift +// Jel +// +// Created by zerocool on 12/13/23. +// + +import SwiftUI +import PulseUI + +struct SettingsView: View { + @Binding var showingSettingsView: Bool + + @StateObject var authState: AuthStateController = AuthStateController.shared + + @ObservedObject var settingsController: SettingsController = SettingsController.shared + var body: some View { + NavigationStack { + Form { + Section { + AppearancePicker() + } + + Section { + NavigationLink { + ConsoleView() + .closeButtonHidden() + } label: { + Text("Logs") + } + + Button(role: .destructive) { + authState.loggedIn = false + authState.save() + } label: { + Text("Sign out") + } + } + } + .navigationTitle("Settings") + .toolbar { + ToolbarItem { + Button { + showingSettingsView.toggle() + settingsController.save() + } label: { + Text("Done") + .bold() + } + } + } + .preferredColorScheme({ + switch settingsController.appearance { + case .dark: + return ColorScheme.dark + case .light: + return ColorScheme.light + case .automatic: + return .none + } + }()) + } + } +} + +#Preview { + SettingsView(showingSettingsView: .constant(true)) +} diff --git a/Jel/Views/SignIn/AddServerView.swift b/Jel/Views/SignIn/AddServerView.swift index 516b982..40c2312 100644 --- a/Jel/Views/SignIn/AddServerView.swift +++ b/Jel/Views/SignIn/AddServerView.swift @@ -9,7 +9,7 @@ import SwiftUI struct AddServerView: View { @EnvironmentObject var jellyfinClient: JellyfinClientController - @ObservedObject var authState: AuthStateController + @ObservedObject var authState: AuthStateController = AuthStateController.shared @Binding var serverUrlIsValid: Bool @State var serverUrlString: String = "http://" @@ -73,7 +73,7 @@ struct AddServerView: View { if isValidUrl(data: serverUrlString) { let url = URL(string: serverUrlString)! jellyfinClient.setUrl(url: url) - if await jellyfinClient.isJellyfinServer() { + if await jellyfinClient.getPublicServerInfo() != nil { authState.serverUrl = url authState.save() urlHasError = false @@ -103,6 +103,6 @@ struct AddServerView: View { } #Preview { - AddServerView(authState: AuthStateController(), serverUrlIsValid: .constant(false)) + AddServerView(serverUrlIsValid: .constant(false)) } diff --git a/Jel/Views/SignIn/SignInToServerView.swift b/Jel/Views/SignIn/SignInToServerView.swift index ae8d82d..259e54d 100644 --- a/Jel/Views/SignIn/SignInToServerView.swift +++ b/Jel/Views/SignIn/SignInToServerView.swift @@ -9,17 +9,19 @@ import SwiftUI struct SignInToServerView: View { @EnvironmentObject var jellyfinClient: JellyfinClientController - @ObservedObject var authState: AuthStateController + @StateObject var authState: AuthStateController = AuthStateController.shared @State var username: String = "" @State var password: String = "" @State var isLoading: Bool = false @State var hasError: Bool = false + @State var serverName: String = "" + var body: some View { VStack { - Text("Sign in") + Text("Sign in to \(serverName)") .font(.title) TextField(text: $username) { Text("Username") @@ -60,6 +62,13 @@ struct SignInToServerView: View { .textFieldStyle(.roundedBorder) .textInputAutocapitalization(.never) .disabled(isLoading) + .onAppear { + Task { + if let serverInfo = await jellyfinClient.getPublicServerInfo() { + serverName = serverInfo.serverName ?? "Unknown" + } + } + } } func logInToServer() async { @@ -74,6 +83,7 @@ struct SignInToServerView: View { } } -#Preview { - SignInToServerView(authState: AuthStateController()) -} +// FIXME: Preview broken for this view +//#Preview { +// SignInToServerView() +//} diff --git a/Jel/Views/SignIn/SignInView.swift b/Jel/Views/SignIn/SignInView.swift index c06788d..729b34b 100644 --- a/Jel/Views/SignIn/SignInView.swift +++ b/Jel/Views/SignIn/SignInView.swift @@ -6,17 +6,20 @@ // import SwiftUI +import PulseUI struct SignInView: View { @EnvironmentObject var jellyfinClient: JellyfinClientController - @ObservedObject var authState: AuthStateController + + @StateObject var authState: AuthStateController = AuthStateController.shared @State var serverUrlIsValid: Bool = false + @State var showingConsoleSheet: Bool = false var body: some View { NavigationStack { - AddServerView(authState: authState, serverUrlIsValid: $serverUrlIsValid) + AddServerView(serverUrlIsValid: $serverUrlIsValid) .navigationDestination(isPresented: $serverUrlIsValid) { - SignInToServerView(authState: authState) + SignInToServerView() } } .onAppear { @@ -31,12 +34,12 @@ struct SignInView: View { return } - if await jellyfinClient.isJellyfinServer() { + if await jellyfinClient.getPublicServerInfo() != nil { serverUrlIsValid = true } } } #Preview { - SignInView(authState: AuthStateController()) + SignInView() } diff --git a/Jel/Views/Utility/ConsoleSheetView.swift b/Jel/Views/Utility/ConsoleSheetView.swift new file mode 100644 index 0000000..a047315 --- /dev/null +++ b/Jel/Views/Utility/ConsoleSheetView.swift @@ -0,0 +1,35 @@ +// +// ConsoleSheetView.swift +// Jel +// +// Created by zerocool on 12/12/23. +// + +import SwiftUI +import PulseUI + +struct ConsoleSheetView: View { + @Binding var showingConsoleSheet: Bool + + var body: some View { + NavigationStack { + ConsoleView() + .closeButtonHidden() + .navigationTitle("Debug consoles") + .toolbar { + ToolbarItem { + Button { + showingConsoleSheet.toggle() + } label: { + Text("Done") + .bold() + } + } + } + } + } +} + +#Preview { + ConsoleSheetView(showingConsoleSheet: .constant(true)) +}