diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift b/JellyfinPlayer tvOS/App/JellyfinPlayer_tvOSApp.swift similarity index 63% rename from JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift rename to JellyfinPlayer tvOS/App/JellyfinPlayer_tvOSApp.swift index 9ab606cb..2f1ff9e7 100644 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOSApp.swift +++ b/JellyfinPlayer tvOS/App/JellyfinPlayer_tvOSApp.swift @@ -7,15 +7,13 @@ import SwiftUI import UIKit + @main struct JellyfinPlayer_tvOSApp: App { - let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { - SplashView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .ignoresSafeArea(.all, edges: .all) + MainCoordinator().view() } } } diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/1280x768-back.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/1280x768-back.png new file mode 100644 index 00000000..f4ec4eda Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/1280x768-back.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..7b0faedf --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "1280x768-back.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 00000000..3d73e5f8 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/512.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/512.png new file mode 100644 index 00000000..b5626a5d Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/512.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..dc3e9968 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "512.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/400x240-back.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/400x240-back.png new file mode 100644 index 00000000..313aad33 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/400x240-back.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..e1178b2e --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "400x240-back.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "Webp.net-resizeimage.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Webp.net-resizeimage.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Webp.net-resizeimage.png new file mode 100644 index 00000000..56bcb845 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Webp.net-resizeimage.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 00000000..3d73e5f8 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/216.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/216.png new file mode 100644 index 00000000..f64eb855 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/216.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..597613ac --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "216.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "Webp.net-resizeimage-2.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Webp.net-resizeimage-2.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Webp.net-resizeimage-2.png new file mode 100644 index 00000000..5059fe96 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Webp.net-resizeimage-2.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000..f47ba43d --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "filename" : "App Icon - App Store.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "1280x768" + }, + { + "filename" : "App Icon.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "400x240" + }, + { + "filename" : "Top Shelf Image Wide.imageset", + "idiom" : "tv", + "role" : "top-shelf-image-wide", + "size" : "2320x720" + }, + { + "filename" : "Top Shelf Image.imageset", + "idiom" : "tv", + "role" : "top-shelf-image", + "size" : "1920x720" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000..d4b5af42 --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "filename" : "top shelf.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "Untitled-1.png", + "idiom" : "tv", + "scale" : "2x" + }, + { + "filename" : "top shelf-1.png", + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "filename" : "Untitled-2.png", + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-1.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-1.png new file mode 100644 index 00000000..897796d6 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-1.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-2.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-2.png new file mode 100644 index 00000000..897796d6 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Untitled-2.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf-1.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf-1.png new file mode 100644 index 00000000..e2f1dd19 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf-1.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf.png new file mode 100644 index 00000000..e2f1dd19 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/top shelf.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..21e50b6b --- /dev/null +++ b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,28 @@ +{ + "images" : [ + { + "filename" : "top shelf.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "Untitled-2.png", + "idiom" : "tv", + "scale" : "2x" + }, + { + "filename" : "top shelf-1.png", + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "filename" : "Untitled-1.png", + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-1.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-1.png new file mode 100644 index 00000000..1ee0e6c4 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-1.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-2.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-2.png new file mode 100644 index 00000000..1ee0e6c4 Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Untitled-2.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf-1.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf-1.png new file mode 100644 index 00000000..6f204dfa Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf-1.png differ diff --git a/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf.png b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf.png new file mode 100644 index 00000000..6f204dfa Binary files /dev/null and b/JellyfinPlayer tvOS/Assets.xcassets/Dev App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/top shelf.png differ diff --git a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift index 37c611ab..e2290ee3 100644 --- a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift +++ b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift @@ -57,6 +57,22 @@ struct PortraitItemElement: View { .opacity(1), alignment: .topTrailing).opacity(1) Text(item.title) .frame(width: 200, height: 30, alignment: .center) + if item.type == "Movie" || item.type == "Series" { + Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else if item.type == "Season" { + Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else { + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } } .onChange(of: envFocused) { envFocus in withAnimation(.linear(duration: 0.15)) { diff --git a/JellyfinPlayer tvOS/Components/PublicUserButton.swift b/JellyfinPlayer tvOS/Components/PublicUserButton.swift index ac49c59a..36943861 100644 --- a/JellyfinPlayer tvOS/Components/PublicUserButton.swift +++ b/JellyfinPlayer tvOS/Components/PublicUserButton.swift @@ -19,7 +19,7 @@ struct PublicUserButton: View { var body: some View { VStack { if publicUser.primaryImageTag != nil { - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) + ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!) .frame(width: 250, height: 250) .cornerRadius(125.0) } else { diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift deleted file mode 100644 index f42803ba..00000000 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ /dev/null @@ -1,176 +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 JellyfinAPI -import SwiftUI - -struct ConnectToServerView: View { - @StateObject var viewModel = ConnectToServerViewModel() - @State var username = "" - @State var password = "" - @State var uri = "" - - var body: some View { - VStack(alignment: .leading) { - if viewModel.isConnectedServer { - if viewModel.publicUsers.isEmpty { - Section(header: Text(viewModel.lastPublicUsers.isEmpty || username == "" ? "Login to \(ServerEnvironment.current.server.name ?? "")": "")) { - if viewModel.lastPublicUsers.isEmpty || username == "" { - TextField(NSLocalizedString("Username", comment: ""), text: $username) - .disableAutocorrection(true) - .autocapitalization(.none) - } else { - HStack { - Spacer() - ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(viewModel.selectedPublicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(viewModel.selectedPublicUser.primaryImageTag ?? "")")!) - .frame(width: 250, height: 250) - .cornerRadius(125.0) - Spacer() - } - } - - SecureField(NSLocalizedString("Password", comment: ""), text: $password) - .disableAutocorrection(true) - .autocapitalization(.none) - } - - Section { - HStack { - Button { - if !viewModel.lastPublicUsers.isEmpty { - username = "" - viewModel.showPublicUsers() - } else { - viewModel.isConnectedServer = false - } - } label: { - Spacer() - HStack { - Text("Back") - } - Spacer() - } - - Button { - viewModel.login() - } label: { - Spacer() - if viewModel.isLoading { - ProgressView() - } else { - Text("Login") - } - Spacer() - }.disabled(viewModel.isLoading || username.isEmpty) - } - } - } else { - VStack { - HStack { - ForEach(viewModel.publicUsers, id: \.id) { publicUser in - Button(action: { - if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { - let user = SessionManager.current.getSavedSession(userID: publicUser.id!) - SessionManager.current.loginWithSavedSession(user: user) - } else { - username = publicUser.name ?? "" - viewModel.selectedPublicUser = publicUser - viewModel.hidePublicUsers() - if !(publicUser.hasPassword ?? true) { - password = "" - viewModel.login() - } - } - }) { - PublicUserButton(publicUser: publicUser) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } - }.padding(.bottom, 20) - HStack { - Spacer() - Button { - viewModel.hidePublicUsers() - username = "" - } label: { - Text("Other User").font(.headline).fontWeight(.semibold) - } - Spacer() - }.padding(.top, 12) - } - } - } else { - if !viewModel.isLoading { - - Form { - Section(header: Text("Server Information")) { - TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.URL) - Button { - viewModel.connectToServer() - } label: { - HStack { - Text("Connect") - Spacer() - } - if viewModel.isLoading { - ProgressView() - } - } - .disabled(viewModel.isLoading || uri.isEmpty) - } - Section(header: Text("Local Servers")) { - if self.viewModel.searching { - ProgressView() - } - ForEach(self.viewModel.servers, id: \.id) { server in - Button(action: { - print(server.url) - viewModel.connectToServer(at: server.url) - }, label: { - HStack { - VStack(alignment: .leading) { - Text(server.name) - .font(.headline) - Text(server.host) - .font(.subheadline) - } - Spacer() - Image(systemName: "chevron.forward") - .padding() - } - - }) - .disabled(viewModel.isLoading) - } - } - .onAppear(perform: self.viewModel.discoverServers) - } - } else { - ProgressView() - } - } - } - .padding(.leading, 90) - .padding(.trailing, 90) - .alert(item: $viewModel.errorMessage) { _ in - Alert(title: Text("Error"), message: Text(viewModel.errorMessage as? String ?? ""), dismissButton: .default(Text("Ok"))) - } - .onChange(of: uri) { uri in - viewModel.uriSubject.send(uri) - } - .onChange(of: username) { username in - viewModel.usernameSubject.send(username) - } - .onChange(of: password) { password in - viewModel.passwordSubject.send(password) - } - .navigationTitle(viewModel.isConnectedServer ? NSLocalizedString("Who's watching?", comment: "") : NSLocalizedString("Connect to Jellyfin", comment: "")) - } -} diff --git a/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements b/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements deleted file mode 100644 index 0273a6a6..00000000 --- a/JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements +++ /dev/null @@ -1,15 +0,0 @@ - - - - - com.apple.developer.user-management - - get-current-user - runs-as-current-user - - keychain-access-groups - - $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain - - - diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/.xccurrentversion b/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/.xccurrentversion deleted file mode 100644 index 0c67376e..00000000 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/JellyfinPlayer_tvOS.xcdatamodel/contents b/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/JellyfinPlayer_tvOS.xcdatamodel/contents deleted file mode 100644 index 9ed2921a..00000000 --- a/JellyfinPlayer tvOS/JellyfinPlayer_tvOS.xcdatamodeld/JellyfinPlayer_tvOS.xcdatamodel/contents +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/JellyfinPlayer tvOS/LibraryView.swift b/JellyfinPlayer tvOS/LibraryView.swift deleted file mode 100644 index e7035067..00000000 --- a/JellyfinPlayer tvOS/LibraryView.swift +++ /dev/null @@ -1,93 +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 SwiftUICollection -import JellyfinAPI - -struct LibraryView: View { - @StateObject var viewModel: LibraryViewModel - var title: String - - // MARK: tracks for grid - var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) - - @State var isShowingSearchView = false - @State var isShowingFilterView = false - - var body: some View { - if viewModel.isLoading == true { - ProgressView() - } else if !viewModel.items.isEmpty { - CollectionView(rows: viewModel.rows) { _, _ in - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .absolute(200), - heightDimension: .absolute(300) - ) - let group = NSCollectionLayoutGroup.horizontal( - layoutSize: groupSize, - subitems: [item] - ) - - let header = - NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(44) - ), - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .topLeading - ) - - let section = NSCollectionLayoutSection(group: group) - - section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) - section.interGroupSpacing = 48 - section.orthogonalScrollingBehavior = .continuous - section.boundarySupplementaryItems = [header] - return section - } cell: { _, cell in - GeometryReader { _ in - if let item = cell.item { - if item.type != "Folder" { - NavigationLink(destination: LazyView { ItemView(item: item) }) { - PortraitItemElement(item: item) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - .onAppear { - if item == viewModel.items.last && viewModel.hasNextPage { - viewModel.requestNextPageAsync() - } - } - } - } else if cell.loadingCell { - ProgressView() - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - } - } - } supplementaryView: { _, indexPath in - HStack { - Spacer() - }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea(.all) - } else { - Text("No results.") - } - } -} - -// stream BM^S by nicki! -// diff --git a/JellyfinPlayer tvOS/MainTabView.swift b/JellyfinPlayer tvOS/MainTabView.swift deleted file mode 100644 index dbad06b5..00000000 --- a/JellyfinPlayer tvOS/MainTabView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// - /* - * 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 - @StateObject private var viewModel = MainTabViewModel() - @State private var backdropAnim: Bool = true - @State private var lastBackdropAnim: Bool = false - - var body: some View { - ZStack { - // please do not touch my magical crossfading. i will wave my magical github wand and cry - if viewModel.lastBackgroundURL != nil { - ImageView(src: viewModel.lastBackgroundURL!, bh: viewModel.backgroundBlurHash) - .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) - .opacity(lastBackdropAnim ? 0.4 : 0) - .ignoresSafeArea() - } - if viewModel.backgroundURL != nil { - ImageView(src: viewModel.backgroundURL!, bh: viewModel.backgroundBlurHash) - .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) - .opacity(backdropAnim ? 0.4 : 0) - .onChange(of: viewModel.backgroundURL) { _ in - lastBackdropAnim = true - backdropAnim = false - withAnimation(.linear(duration: 0.33)) { - lastBackdropAnim = false - backdropAnim = true - } - } - .ignoresSafeArea() - } - - TabView(selection: $tabSelection) { - HomeView() - .offset(y: -1) // don't remove this. it breaks tabview on 4K displays. - .tabItem { - Text("Home") - Image(systemName: "house") - } - .tag(Tab.home) - - LibraryListView() - .tabItem { - Text("All Media") - Image(systemName: "folder") - } - .tag(Tab.allMedia) - - SettingsView(viewModel: SettingsViewModel()) - .offset(y: -1) // don't remove this. it breaks tabview on 4K displays. - .tabItem { - Text("Settings") - Image(systemName: "gear") - } - .tag(Tab.settings) - } - } - } -} - -extension MainTabView { - enum Tab: String { - case home - case allMedia - case settings - } -} - -// stream ancient dreams in a modern land by MARINA! diff --git a/JellyfinPlayer tvOS/PersistenceController.swift b/JellyfinPlayer tvOS/PersistenceController.swift deleted file mode 100644 index 056d1330..00000000 --- a/JellyfinPlayer tvOS/PersistenceController.swift +++ /dev/null @@ -1,37 +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 CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - let container: NSPersistentContainer - - init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "Model") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (_, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/JellyfinPlayer tvOS/README.md b/JellyfinPlayer tvOS/README.md deleted file mode 100644 index 59394731..00000000 --- a/JellyfinPlayer tvOS/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Design Notes - -tvos is dumb and how I got around the ScrollViews clipping requires ALL interface elements to have a leading and trailing padding of _~~135~~ something else but i forgot_ pt to align with the original "safe area bounds" - diff --git a/JellyfinPlayer tvOS/SplashView.swift b/JellyfinPlayer tvOS/SplashView.swift deleted file mode 100644 index 3756ecd9..00000000 --- a/JellyfinPlayer tvOS/SplashView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -/* - * 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 { - Group { - if viewModel.isLoggedIn { - NavigationView { - MainTabView() - }.padding(.all, -1) - } else { - NavigationView { - ConnectToServerView() - } - .navigationViewStyle(StackNavigationViewStyle()) - } - } - } -} diff --git a/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift b/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift new file mode 100644 index 00000000..2ff8fe9b --- /dev/null +++ b/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift @@ -0,0 +1,52 @@ +// + /* + * 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 Defaults +import Stinsen +import SwiftUI + +struct BasicAppSettingsView: View { + + @EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router + @ObservedObject var viewModel: BasicAppSettingsViewModel + @State var resetTapped: Bool = false + + @Default(.appAppearance) var appAppearance + + var body: some View { + Form { + Section { + Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { + ForEach(self.viewModel.appearances, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) + } + }.onChange(of: appAppearance, perform: { _ in + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style + }) + } header: { + Text("Accessibility") + } + + Button { + resetTapped = true + } label: { + Text("Reset") + } + } + .alert("Reset", isPresented: $resetTapped, actions: { + Button(role: .destructive) { + viewModel.reset() + basicAppSettingsRouter.dismissCoordinator() + } label: { + Text("Reset") + } + }) + .navigationTitle("Settings") + } +} diff --git a/JellyfinPlayer tvOS/Views/ConnectToServerView.swift b/JellyfinPlayer tvOS/Views/ConnectToServerView.swift new file mode 100644 index 00000000..ce981f88 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ConnectToServerView.swift @@ -0,0 +1,73 @@ +/* + * 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 Stinsen + +struct ConnectToServerView: View { + + @StateObject var viewModel = ConnectToServerViewModel() + @State var uri = "" + + var body: some View { + List { + Section { + TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) + .disableAutocorrection(true) + .autocapitalization(.none) + .keyboardType(.URL) + Button { + viewModel.connectToServer(uri: uri) + } label: { + HStack { + Text("Connect") + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + } + .disabled(viewModel.isLoading || uri.isEmpty) + } header: { + Text("Connect to a Jellyfin server") + } + + Section(header: Text("Local Servers")) { + if viewModel.searching { + ProgressView() + } + ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in + Button(action: { + viewModel.connectToServer(uri: discoveredServer.url.absoluteString) + }, label: { + HStack { + Text(discoveredServer.name) + .font(.headline) + Text("• \(discoveredServer.host)") + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + + }) + } + } + .onAppear(perform: self.viewModel.discoverServers) + .headerProminence(.increased) + } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), + dismissButton: .cancel()) + } + .navigationTitle("Connect") + } +} diff --git a/JellyfinPlayer tvOS/ContinueWatchingView.swift b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift similarity index 81% rename from JellyfinPlayer tvOS/ContinueWatchingView.swift rename to JellyfinPlayer tvOS/Views/ContinueWatchingView.swift index ad793923..0ca800bd 100644 --- a/JellyfinPlayer tvOS/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift @@ -9,11 +9,14 @@ import SwiftUI import JellyfinAPI import Combine +import Stinsen struct ContinueWatchingView: View { var items: [BaseItemDto] @Namespace private var namespace + var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() + var body: some View { VStack(alignment: .leading) { if items.count > 0 { @@ -25,14 +28,16 @@ struct ContinueWatchingView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { + Button { + self.homeRouter?.route(to: \.modalItem, item) + } label: { LandscapeItemElement(item: item) } .buttonStyle(PlainNavigationLinkButtonStyle()) } Spacer().frame(width: 45) } - }.frame(height: 330) + }.frame(height: 350) } else { EmptyView() } diff --git a/JellyfinPlayer tvOS/HomeView.swift b/JellyfinPlayer tvOS/Views/HomeView.swift similarity index 85% rename from JellyfinPlayer tvOS/HomeView.swift rename to JellyfinPlayer tvOS/Views/HomeView.swift index 8642a1a6..6ab1d03f 100644 --- a/JellyfinPlayer tvOS/HomeView.swift +++ b/JellyfinPlayer tvOS/Views/HomeView.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI struct HomeView: View { + @EnvironmentObject var homeRouter: HomeCoordinator.Router @StateObject var viewModel = HomeViewModel() @State var showingSettings = false @@ -33,9 +34,9 @@ struct HomeView: View { VStack(alignment: .leading) { let library = viewModel.libraries.first(where: { $0.id == libraryID }) - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") - }) { + Button { + self.homeRouter.route(to: \.modalLibrary, (.init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")) + } label: { HStack { Text("Latest \(library?.name ?? "")") .font(.headline) diff --git a/JellyfinPlayer tvOS/EpisodeItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/EpisodeItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift diff --git a/JellyfinPlayer tvOS/ItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift similarity index 79% rename from JellyfinPlayer tvOS/ItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/ItemView.swift index 62692415..b62d0702 100644 --- a/JellyfinPlayer tvOS/ItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift @@ -9,6 +9,19 @@ import SwiftUI import Introspect import JellyfinAPI +// Useless view necessary in tvOS because of iOS's implementation +struct ItemNavigationView: View { + private let item: BaseItemDto + + init(item: BaseItemDto) { + self.item = item + } + + var body: some View { + ItemView(item: item) + } +} + struct ItemView: View { private var item: BaseItemDto diff --git a/JellyfinPlayer tvOS/MovieItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/MovieItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/MovieItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/MovieItemView.swift diff --git a/JellyfinPlayer tvOS/SeasonItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/SeasonItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift diff --git a/JellyfinPlayer tvOS/SeriesItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/SeriesItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift diff --git a/JellyfinPlayer tvOS/LatestMediaView.swift b/JellyfinPlayer tvOS/Views/LatestMediaView.swift similarity index 88% rename from JellyfinPlayer tvOS/LatestMediaView.swift rename to JellyfinPlayer tvOS/Views/LatestMediaView.swift index 582dd035..92be14d8 100644 --- a/JellyfinPlayer tvOS/LatestMediaView.swift +++ b/JellyfinPlayer tvOS/Views/LatestMediaView.swift @@ -27,7 +27,7 @@ struct LatestMediaView: View { viewDidLoad = true DispatchQueue.global(qos: .userInitiated).async { - UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12) + UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { response in @@ -48,7 +48,7 @@ struct LatestMediaView: View { } Spacer().frame(width: 45) } - }.frame(height: 396) + }.frame(height: 480) .onAppear(perform: onAppear) } } diff --git a/JellyfinPlayer tvOS/Views/LibraryFilterView.swift b/JellyfinPlayer tvOS/Views/LibraryFilterView.swift new file mode 100644 index 00000000..daf9c75c --- /dev/null +++ b/JellyfinPlayer tvOS/Views/LibraryFilterView.swift @@ -0,0 +1,94 @@ +/* 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 JellyfinAPI +import Stinsen +import SwiftUI + +struct LibraryFilterView: View { + + @EnvironmentObject var filterRouter: FilterCoordinator.Router + @Binding var filters: LibraryFilters + var parentId: String = "" + + @StateObject var viewModel: LibraryFilterViewModel + + init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { + _filters = filters + self.parentId = parentId + _viewModel = + StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) + } + + var body: some View { + VStack { + if viewModel.isLoading { + ProgressView() + } else { + Form { + if viewModel.enabledFilterType.contains(.genre) { + MultiSelector(label: NSLocalizedString("Genres", comment: ""), + options: viewModel.possibleGenres, + optionToString: { $0.name ?? "" }, + selected: $viewModel.modifiedFilters.withGenres) + } + if viewModel.enabledFilterType.contains(.filter) { + MultiSelector(label: NSLocalizedString("Filters", comment: ""), + options: viewModel.possibleItemFilters, + optionToString: { $0.localized }, + selected: $viewModel.modifiedFilters.filters) + } + if viewModel.enabledFilterType.contains(.tag) { + MultiSelector(label: NSLocalizedString("Tags", comment: ""), + options: viewModel.possibleTags, + optionToString: { $0 }, + selected: $viewModel.modifiedFilters.tags) + } + if viewModel.enabledFilterType.contains(.sortBy) { + Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) { + ForEach(viewModel.possibleSortBys, id: \.self) { so in + Text(so.localized).tag(so) + } + } + } + if viewModel.enabledFilterType.contains(.sortOrder) { + Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) { + ForEach(viewModel.possibleSortOrders, id: \.self) { so in + Text(so.rawValue).tag(so) + } + } + } + } + Button { + viewModel.resetFilters() + self.filters = viewModel.modifiedFilters + filterRouter.dismissCoordinator() + } label: { + Text("Reset") + } + } + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + filterRouter.dismissCoordinator() + } label: { + Image(systemName: "xmark") + } + } + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + viewModel.updateModifiedFilter() + self.filters = viewModel.modifiedFilters + filterRouter.dismissCoordinator() + } label: { + Text("Apply") + } + } + } + } +} diff --git a/JellyfinPlayer tvOS/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift similarity index 55% rename from JellyfinPlayer tvOS/LibraryListView.swift rename to JellyfinPlayer tvOS/Views/LibraryListView.swift index 2495888a..d7ecd279 100644 --- a/JellyfinPlayer tvOS/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -16,47 +16,11 @@ struct LibraryListView: View { var body: some View { ScrollView { LazyVStack { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") - }) { - ZStack { - HStack { - Spacer() - Text("Your Favorites") - .font(.subheadline) - .fontWeight(.semibold) - Spacer() - } - } - .padding(16) - .frame(minWidth: 100, maxWidth: .infinity) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 5) - - NavigationLink(destination: LazyView { - Text("WIP") - }) { - ZStack { - HStack { - Spacer() - Text("All Genres") - .font(.subheadline) - .fontWeight(.semibold) - Spacer() - } - } - .padding(16) - .frame(minWidth: 100, maxWidth: .infinity) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 15) - if !viewModel.isLoading { ForEach(viewModel.libraries, id: \.id) { library in - if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { + if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" { + EmptyView() + } else { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") }) { @@ -80,8 +44,6 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 5) - } else { - EmptyView() } } } else { @@ -91,15 +53,5 @@ struct LibraryListView: View { .padding(.trailing, 16) .padding(.top, 8) } - .navigationTitle(NSLocalizedString("All Media", comment: "")) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - NavigationLink(destination: LazyView { - LibrarySearchView(viewModel: .init(parentID: nil)) - }) { - Image(systemName: "magnifyingglass") - } - } - } } } diff --git a/JellyfinPlayer tvOS/LibrarySearchView.swift b/JellyfinPlayer tvOS/Views/LibrarySearchView.swift similarity index 100% rename from JellyfinPlayer tvOS/LibrarySearchView.swift rename to JellyfinPlayer tvOS/Views/LibrarySearchView.swift diff --git a/JellyfinPlayer tvOS/Views/LibraryView.swift b/JellyfinPlayer tvOS/Views/LibraryView.swift new file mode 100644 index 00000000..ca272c9e --- /dev/null +++ b/JellyfinPlayer tvOS/Views/LibraryView.swift @@ -0,0 +1,101 @@ +/* + * 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 SwiftUICollection +import JellyfinAPI + +struct LibraryView: View { + @EnvironmentObject var libraryRouter: LibraryCoordinator.Router + @StateObject var viewModel: LibraryViewModel + var title: String + + // MARK: tracks for grid + var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) + + @State var isShowingSearchView = false + @State var isShowingFilterView = false + + var body: some View { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button { + libraryRouter.route(to: \.modalItem, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + .onAppear { + if item == viewModel.items.last && viewModel.hasNextPage { + viewModel.requestNextPageAsync() + } + } + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } + } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + VStack { + Text("No results.") + Button { } label: { + Text("Reload") + } + } + } + } +} + +// stream BM^S by nicki! +// diff --git a/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift new file mode 100644 index 00000000..9e388718 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift @@ -0,0 +1,89 @@ +/* + * 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 SwiftUICollection +import JellyfinAPI + +struct MovieLibrariesView: View { + @EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router + @StateObject var viewModel: MovieLibrariesViewModel + var title: String + + var body: some View { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button { + self.movieLibrariesRouter.route(to: \.library, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } + } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + VStack { + Text("No results.") + Button { + print("movieLibraries reload") + } label: { + Text("Reload") + } + } + } + } +} diff --git a/JellyfinPlayer tvOS/NextUpView.swift b/JellyfinPlayer tvOS/Views/NextUpView.swift similarity index 80% rename from JellyfinPlayer tvOS/NextUpView.swift rename to JellyfinPlayer tvOS/Views/NextUpView.swift index 1db5d360..3de41d6b 100644 --- a/JellyfinPlayer tvOS/NextUpView.swift +++ b/JellyfinPlayer tvOS/Views/NextUpView.swift @@ -9,9 +9,12 @@ import SwiftUI import JellyfinAPI import Combine +import Stinsen struct NextUpView: View { var items: [BaseItemDto] + + var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() var body: some View { VStack(alignment: .leading) { @@ -24,13 +27,15 @@ struct NextUpView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { + Button { + self.homeRouter?.route(to: \.modalItem, item) + } label: { LandscapeItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) } Spacer().frame(width: 45) } - }.frame(height: 330) + }.frame(height: 350) .offset(y: -10) } else { EmptyView() diff --git a/JellyfinPlayer tvOS/Views/ServerDetailView.swift b/JellyfinPlayer tvOS/Views/ServerDetailView.swift new file mode 100644 index 00000000..74c88ce6 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ServerDetailView.swift @@ -0,0 +1,62 @@ +// + /* + * 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 ServerDetailView: View { + + @ObservedObject var viewModel = ServerDetailViewModel() + + var body: some View { + Form { + Section(header: Text("Server Details")) { + HStack { + Text("Name") + Spacer() + Text(SessionManager.main.currentLogin.server.name) + .foregroundColor(.secondary) + } + + HStack { + Text("URI") + Spacer() + Text(SessionManager.main.currentLogin.server.uri) + .foregroundColor(.secondary) + } + + HStack { + Text("Version") + Spacer() + Text(SessionManager.main.currentLogin.server.version) + .foregroundColor(.secondary) + } + + HStack { + Text("Operating System") + Spacer() + Text(SessionManager.main.currentLogin.server.os) + .foregroundColor(.secondary) + } + } + + Button(action: { + viewModel.refreshServerLibrary() + }, label: { + HStack { + Text("Refresh Library") + .font(.callout) + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + }).disabled(viewModel.isLoading) + } + } +} diff --git a/JellyfinPlayer tvOS/Views/ServerListView.swift b/JellyfinPlayer tvOS/Views/ServerListView.swift new file mode 100644 index 00000000..5ca66b3f --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ServerListView.swift @@ -0,0 +1,125 @@ +// + /* + * 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 CoreStore +import SwiftUI + +struct ServerListView: View { + + @EnvironmentObject var serverListRouter: ServerListCoordinator.Router + @ObservedObject var viewModel: ServerListViewModel + + @ViewBuilder + private var listView: some View { + ScrollView { + LazyVStack { + ForEach(viewModel.servers, id: \.id) { server in + Button { + serverListRouter.route(to: \.userList, server) + } label: { + HStack { + Image(systemName: "server.rack") + .font(.system(size: 72)) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 5) { + Text(server.name) + .font(.title2) + .foregroundColor(.primary) + + Text(server.uri) + .font(.footnote) + .disabled(true) + .foregroundColor(.secondary) + + Text(viewModel.userTextFor(server: server)) + .font(.footnote) + .foregroundColor(.primary) + } + + Spacer() + } + } + .padding(.horizontal, 100) + .contextMenu { + Button(role: .destructive) { + viewModel.remove(server: server) + } label: { + Label("Remove", systemImage: "trash") + } + } + } + } + .padding(.top, 50) + } + .padding(.top, 50) + } + + @ViewBuilder + private var noServerView: some View { + VStack { + Text("Connect to a Jellyfin server to get started") + .frame(minWidth: 50, maxWidth: 500) + .multilineTextAlignment(.center) + .font(.callout) + + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + Text("Connect") + .bold() + .font(.callout) + } + .padding(.top, 40) + } + } + + @ViewBuilder + private var innerBody: some View { + if viewModel.servers.isEmpty { + noServerView + .offset(y: -50) + } else { + listView + } + } + + @ViewBuilder + private var trailingToolbarContent: some View { + if viewModel.servers.isEmpty { + EmptyView() + } else { + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + Image(systemName: "plus.circle.fill") + } + .contextMenu { + Button { + serverListRouter.route(to: \.basicAppSettings) + } label: { + Text("Settings") + } + } + } + } + + var body: some View { + innerBody + .navigationTitle("Servers") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + trailingToolbarContent + } + } + .onAppear { + viewModel.fetchServers() + } + } +} diff --git a/JellyfinPlayer tvOS/SettingsView.swift b/JellyfinPlayer tvOS/Views/SettingsView.swift similarity index 75% rename from JellyfinPlayer tvOS/SettingsView.swift rename to JellyfinPlayer tvOS/Views/SettingsView.swift index 97916b7c..7442eeeb 100644 --- a/JellyfinPlayer tvOS/SettingsView.swift +++ b/JellyfinPlayer tvOS/Views/SettingsView.swift @@ -8,10 +8,10 @@ import CoreData import SwiftUI import Defaults +import JellyfinAPI struct SettingsView: View { - @Environment(\.managedObjectContext) private var viewContext - + @ObservedObject var viewModel: SettingsViewModel @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @@ -19,11 +19,6 @@ struct SettingsView: View { @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode - @State private var username: String = "" - - func onAppear() { - username = SessionManager.current.user?.username ?? "" - } var body: some View { Form { @@ -61,30 +56,23 @@ struct SettingsView: View { ) } - Section(header: Text(ServerEnvironment.current.server.name ?? "")) { + Section(header: Text(SessionManager.main.currentLogin.server.name)) { HStack { - Text("Signed in as \(username)").foregroundColor(.primary) + Text("Signed in as \(SessionManager.main.currentLogin.user.username)").foregroundColor(.primary) Spacer() Button { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) } label: { Text("Switch user").font(.callout) } } Button { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } + SessionManager.main.logout() } label: { Text("Sign out").font(.callout) } } - }.onAppear(perform: onAppear) + } .padding(.leading, 90) .padding(.trailing, 90) } diff --git a/JellyfinPlayer tvOS/Views/TVLibrariesView.swift b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift new file mode 100644 index 00000000..3ae4d8df --- /dev/null +++ b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift @@ -0,0 +1,89 @@ +/* + * 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 SwiftUICollection +import JellyfinAPI + +struct TVLibrariesView: View { + @EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router + @StateObject var viewModel: TVLibrariesViewModel + var title: String + + var body: some View { + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { _, _ in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .absolute(200), + heightDimension: .absolute(300) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: groupSize, + subitems: [item] + ) + + let header = + NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .absolute(44) + ), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .topLeading + ) + + let section = NSCollectionLayoutSection(group: group) + + section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) + section.interGroupSpacing = 48 + section.orthogonalScrollingBehavior = .continuous + section.boundarySupplementaryItems = [header] + return section + } cell: { _, cell in + GeometryReader { _ in + if let item = cell.item { + if item.type != "Folder" { + Button { + self.tvLibrariesRouter.route(to: \.library, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + } else if cell.loadingCell { + ProgressView() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } + } + } supplementaryView: { _, indexPath in + HStack { + Spacer() + }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } else { + VStack { + Text("No results.") + Button { + print("tvLibraries reload") + } label: { + Text("Reload") + } + } + } + } +} diff --git a/JellyfinPlayer tvOS/Views/UserListView.swift b/JellyfinPlayer tvOS/Views/UserListView.swift new file mode 100644 index 00000000..85bcbe52 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/UserListView.swift @@ -0,0 +1,107 @@ +// + /* + * 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 UserListView: View { + + @EnvironmentObject var userListRouter: UserListCoordinator.Router + @ObservedObject var viewModel: UserListViewModel + + @ViewBuilder + private var listView: some View { + ScrollView { + LazyVStack { + ForEach(viewModel.users, id: \.id) { user in + Button { + viewModel.login(user: user) + } label: { + HStack { + Text(user.username) + .font(.title2) + + Spacer() + + if viewModel.isLoading { + ProgressView() + } + } + } + .padding(.horizontal, 100) + .contextMenu { + Button(role: .destructive) { + viewModel.remove(user: user) + } label: { + Label("Remove", systemImage: "trash") + } + } + } + } + .padding(.top, 50) + } + .padding(.top, 50) + } + + @ViewBuilder + private var noUserView: some View { + VStack { + Text("Sign in to get started") + .frame(minWidth: 50, maxWidth: 500) + .multilineTextAlignment(.center) + .font(.callout) + + Button { + userListRouter.route(to: \.userSignIn, viewModel.server) + } label: { + Text("Sign in") + .bold() + .font(.callout) + } + .padding(.top, 40) + } + } + + @ViewBuilder + private var innerBody: some View { + if viewModel.users.isEmpty { + noUserView + .offset(y: -50) + } else { + listView + } + } + + @ViewBuilder + private var toolbarContent: some View { + if viewModel.users.isEmpty { + EmptyView() + } else { + HStack { + Button { + userListRouter.route(to: \.userSignIn, viewModel.server) + } label: { + Image(systemName: "person.crop.circle.fill.badge.plus") + } + } + } + } + + var body: some View { + innerBody + .navigationTitle(viewModel.server.name) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + toolbarContent + } + } + .onAppear { + viewModel.fetchUsers() + } + } +} diff --git a/JellyfinPlayer tvOS/Views/UserSignInView.swift b/JellyfinPlayer tvOS/Views/UserSignInView.swift new file mode 100644 index 00000000..f9b59ba6 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/UserSignInView.swift @@ -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 SwiftUI +import Stinsen + +struct UserSignInView: View { + + @ObservedObject var viewModel: UserSignInViewModel + @State private var username: String = "" + @State private var password: String = "" + + var body: some View { + Form { + + Section { + TextField("Username", text: $username) + .disableAutocorrection(true) + .autocapitalization(.none) + + SecureField("Password", text: $password) + .disableAutocorrection(true) + .autocapitalization(.none) + + Button { + viewModel.login(username: username, password: password) + } label: { + HStack { + Text("Connect") + Spacer() + if viewModel.isLoading { + ProgressView() + } + } + } + .disabled(viewModel.isLoading || username.isEmpty) + + } header: { + Text("Sign In to \(viewModel.server.name)") + } + } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), + dismissButton: .cancel()) + } + .navigationTitle("Sign In") + } +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/AudioView.swift similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/AudioView.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/AudioView.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/InfoTabBarViewController.swift similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/InfoTabBarViewController.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/MediaInfoView.swift similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/MediaInfoView.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/SubtitlesView.swift similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/SubtitlesView.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.storyboard similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard rename to JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.storyboard diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift similarity index 100% rename from JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift similarity index 95% rename from JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift rename to JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift index 875fcd06..0c4c5bae 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift @@ -138,15 +138,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() + + let currentUser = SessionManager.main.currentLogin.user - guard let currentUser = SessionManager.current.user else { - return - } - - let playbackInfo = PlaybackInfoDto(userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) + let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) DispatchQueue.global(qos: .userInitiated).async { [self] in - MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) .sink(receiveCompletion: { result in print(result) }, receiveValue: { [self] response in @@ -166,12 +164,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Item is being transcoded by request of server if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode - streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! + streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(transcodiungUrl)")! } // Item will be directly played by the client else { item.videoType = .directPlay - streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! +// streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! + streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! } item.videoUrl = streamURL @@ -186,7 +185,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 9047973a..a07f53b3 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ 5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */; }; 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A16268B919A003024C9 /* SeriesItemView.swift */; }; 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A18268B947A003024C9 /* PlainLinkButton.swift */; }; - 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EE267ABF72005D8AB9 /* NextUpView.swift */; }; @@ -37,14 +36,12 @@ 5321753E2671DE9C005491E6 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */; }; - 53272535268BF9710035FBF1 /* SwiftUIFocusGuide in Frameworks */ = {isa = PBXBuildFile; productRef = 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */; }; 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */; }; 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53272538268C20100035FBF1 /* EpisodeItemView.swift */; }; 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */; }; 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; - 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; }; 534D4FF026A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF126A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 534D4FF226A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; @@ -57,12 +54,8 @@ 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */; }; 535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; }; 5358706A2669D21700D05A09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870692669D21700D05A09 /* Preview Assets.xcassets */; }; - 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5358706B2669D21700D05A09 /* PersistenceController.swift */; }; 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; - 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5358708C2669D7A800D05A09 /* KeychainSwift */; }; 535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; }; - 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5358709A2669D7A800D05A09 /* NukeUI */; }; - 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; @@ -71,7 +64,6 @@ 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; }; - 53628C6D26B5AA0D008A64A0 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 53628C6C26B5AA0D008A64A0 /* Defaults */; }; 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAC269CFAEA00A2D8B7 /* Puppy */; }; 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAE269CFAF600A2D8B7 /* Puppy */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; @@ -92,8 +84,6 @@ 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.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 */; }; - 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; }; 53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; }; 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; }; @@ -145,12 +135,10 @@ 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; }; 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A83C32268A309300DF3D92 /* LibraryView.swift */; }; 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53ABFDDB267972BF00886593 /* TVServices.framework */; }; - 53ABFDDE267974E300886593 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDDD267974E300886593 /* SplashView.swift */; }; 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; }; - 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; }; 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; }; @@ -162,7 +150,6 @@ 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBFE1F64394BCC2EFFF1610D /* Pods_JellyfinPlayer_tvOS.framework */; }; 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */; }; - 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53EC6E24267EB10F006DD26A /* SwiftyJSON */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; }; 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; }; @@ -170,7 +157,6 @@ 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; - 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; @@ -178,25 +164,17 @@ 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; - 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; - 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; - 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 6220D0C826D63F3700B8E046 /* Stinsen */; }; 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; - 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; - 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; - 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.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 */; }; - 6260FFF926A09754003FA968 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 6260FFF826A09754003FA968 /* CombineExt */; }; - 6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 6261A0DF26A0AB710072EF1C /* CombineExt */; }; 62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; @@ -212,23 +190,15 @@ 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95262670CABD0091AF3B /* NextUpWidget.swift */; }; 628B95292670CABE0091AF3B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 628B95282670CABE0091AF3B /* Assets.xcassets */; }; 628B952D2670CABE0091AF3B /* WidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95322670CAEA0091AF3B /* NukeUI */; }; 628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95342670CAEA0091AF3B /* JellyfinAPI */; }; 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95362670CB800091AF3B /* JellyfinWidget.swift */; }; - 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; - 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; }; 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 62C29E9B26D0FE4200C1D2E7 /* Stinsen */; }; - 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; - 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */; }; + 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* iOSMainCoordinator.swift */; }; + 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA026D102A500C1D2E7 /* iOSMainTabCoordinator.swift */; }; 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; - 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; }; - 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; - 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; - 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; - 62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -247,24 +217,73 @@ 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; - 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; - 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; - 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; + C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; }; + C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; }; + C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; }; + C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; }; + C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; + C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; - C49FB6592717A06300AAEABB /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C49FB6582717A06300AAEABB /* SwiftUICollection */; }; - C4BFD4E527167B63007739E3 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C4BFD4E427167B63007739E3 /* SwiftUICollection */; }; + C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; + C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; + C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; + C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; + C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; + C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; + C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; }; + C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; + E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; + E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; }; + E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; }; + E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; }; + E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; }; + E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9F271A2CF200EA0737 /* Nuke */; }; + E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; }; E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; + E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BC27163C63009D4DAF /* EmailHelper.swift */; }; + E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; + E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; + E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; + E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3C52716499E009D4DAF /* CoreStore */; }; + E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; + E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; + E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; + E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */; }; + E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CC27164CA7009D4DAF /* CoreStore */; }; + E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3CE27164E1F009D4DAF /* CoreStore */; }; + E13DD3D327168E65009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3D227168E65009D4DAF /* Defaults */; }; + E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */; }; + E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E13DD3DC27175CE3009D4DAF /* Defaults */; }; + E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; + E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */; }; + E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E427177D15009D4DAF /* ServerListView.swift */; }; + E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */; }; + E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */; }; + E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */; }; + E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */; }; + E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */; }; + E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */; }; + E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F4271793BB009D4DAF /* UserSignInView.swift */; }; + E13DD3F72717E87D009D4DAF /* SwiftfinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */; }; + E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; + E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; + E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; }; + E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; }; E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; }; E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; @@ -275,17 +294,62 @@ E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; }; E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; }; + E19169CE272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; + E19169CF272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; + E19169D0272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; }; + E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; }; + E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; }; + E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; + E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; + E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; + E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; + E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; + E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; + E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; + E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; + E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; + E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */; }; + E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */; }; + E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; }; + E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */; }; + E193D53E27193F9A00900D82 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; + E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */; }; + E193D547271941C500900D82 /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D546271941C500900D82 /* UserListView.swift */; }; + E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; }; + E193D54B271941D300900D82 /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54A271941D300900D82 /* ServerListView.swift */; }; + E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54C2719426600900D82 /* LibraryFilterView.swift */; }; + E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; }; + E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; + E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; + E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* SwiftUICollection */; }; + E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A9999A271A343C008E78C0 /* SwiftUICollection */; }; E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */; }; - E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */; }; E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; }; + E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; }; + E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; }; + E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */; }; + E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; }; + E1D4BF7F2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; }; + E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; + E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; + E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */; }; + E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */; }; + E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF862719D27100A11E64 /* Bitrates.swift */; }; + E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF862719D27100A11E64 /* Bitrates.swift */; }; + E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */; }; + E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */; }; + E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; }; + E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; + E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; }; + E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; @@ -345,12 +409,10 @@ 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; 53116A16268B919A003024C9 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; 53116A18268B947A003024C9 /* PlainLinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLinkButton.swift; sourceTree = ""; }; - 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; 531690EE267ABF72005D8AB9 /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = ""; }; - 531690F8267AD135005D8AB9 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = ""; }; 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; @@ -368,7 +430,6 @@ 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayer_tvOSApp.swift; sourceTree = ""; }; 535870662669D21700D05A09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 535870692669D21700D05A09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 5358706B2669D21700D05A09 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 535870702669D21700D05A09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = ""; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; @@ -401,8 +462,6 @@ 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 5377CBFD263B596B003A4E83 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; - 5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */ = {isa = PBXFileReference; explicitFileType = wrapper.xcdatamodel; path = JellyfinPlayer.xcdatamodel; sourceTree = ""; }; 5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; 5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; @@ -424,9 +483,7 @@ 5398514426B64DA100101B49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 53A83C32268A309300DF3D92 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; - 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "JellyfinPlayer tvOS.entitlements"; sourceTree = ""; }; 53ABFDDB267972BF00886593 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.0.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; - 53ABFDDD267974E300886593 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = ""; }; 53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; @@ -448,14 +505,11 @@ 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = ""; }; 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = ""; }; 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = ""; }; - 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = ""; }; 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = ""; }; 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; - 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; - 625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; @@ -473,13 +527,11 @@ 628B95282670CABE0091AF3B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; - 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; - 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; - 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabCoordinator.swift; sourceTree = ""; }; + 62C29E9E26D1016600C1D2E7 /* iOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSMainCoordinator.swift; sourceTree = ""; }; + 62C29EA026D102A500C1D2E7 /* iOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSMainTabCoordinator.swift; sourceTree = ""; }; 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = ""; }; 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = ""; }; - 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; @@ -489,18 +541,41 @@ 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemViewModel.swift; sourceTree = ""; }; 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterViewModel.swift; sourceTree = ""; }; 62E632F2267D54030063E547 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = ""; }; 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = ""; }; + C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = ""; }; + C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = ""; }; + C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = ""; }; + C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = ""; }; + C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; + C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; + C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = ""; }; DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; + E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; + E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; + E13DD3BC27163C63009D4DAF /* EmailHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailHelper.swift; sourceTree = ""; }; + E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; + E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = ""; }; + E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStoreDefaults.swift; sourceTree = ""; }; + E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = ""; }; + E13DD3E427177D15009D4DAF /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = ""; }; + E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListCoordinator.swift; sourceTree = ""; }; + E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInViewModel.swift; sourceTree = ""; }; + E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinNotificationCenter.swift; sourceTree = ""; }; + E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInCoordinator.swift; sourceTree = ""; }; + E13DD3F4271793BB009D4DAF /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; + E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; + E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; + E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = ""; }; E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = ""; }; E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; @@ -510,12 +585,30 @@ E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = ""; }; E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = ""; }; E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCardVStackView.swift; sourceTree = ""; }; + E19169CD272514760085832A /* HTTPScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPScheme.swift; sourceTree = ""; }; + E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitImageStackable.swift; sourceTree = ""; }; + E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = ""; }; + E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = ""; }; + E193D546271941C500900D82 /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; + E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; + E193D54A271941D300900D82 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = ""; }; + E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; + E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; + E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillHStackView.swift; sourceTree = ""; }; E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = ""; }; E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitHeaderOverlayView.swift; sourceTree = ""; }; + E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; + E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsViewModel.swift; sourceTree = ""; }; + E1D4BF802719D22800A11E64 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = ""; }; + E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackLanguage.swift; sourceTree = ""; }; + E1D4BF862719D27100A11E64 /* Bitrates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bitrates.swift; sourceTree = ""; }; + E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = ""; }; + E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; + E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = ""; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; @@ -528,18 +621,17 @@ buildActionMask = 2147483647; files = ( 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */, + E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, + E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, - 6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */, - 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */, - 53272535268BF9710035FBF1 /* SwiftUIFocusGuide in Frameworks */, - 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, - C49FB6592717A06300AAEABB /* SwiftUICollection in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, - 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */, + E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */, + E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */, + E12186DE2718F1C50010884C /* Defaults in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -548,17 +640,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E13DD3D327168E65009D4DAF /* Defaults in Frameworks */, 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */, 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, - 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, - 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, - C4BFD4E527167B63007739E3 /* SwiftUICollection in Frameworks */, - 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, + E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */, 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, + E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */, + E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, - 621C638026672A30004216EA /* NukeUI in Frameworks */, + E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, - 6260FFF926A09754003FA968 /* CombineExt in Frameworks */, + E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */, 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -567,27 +659,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 53628C6D26B5AA0D008A64A0 /* Defaults in Frameworks */, - 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */, 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, + E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */, 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */, 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, - 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */, + E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */, 628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */, + E1218C9C271A26C400EA0737 /* Nuke in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 091B5A852683142E00D78B61 /* ServerLocator */ = { + 091B5A852683142E00D78B61 /* ServerDiscovery */ = { isa = PBXGroup; children = ( 091B5A872683142E00D78B61 /* ServerDiscovery.swift */, 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */, ); - path = ServerLocator; + path = ServerDiscovery; sourceTree = ""; }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { @@ -607,25 +699,29 @@ 532175392671BCED005491E6 /* ViewModels */ = { isa = PBXGroup; children = ( + E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, - 62E632F2267D54030063E547 /* ItemViewModel.swift */, 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, + 62E632F2267D54030063E547 /* ItemViewModel.swift */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, + C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, + C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, + E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, - 625CB5692678B71200530A6E /* SplashViewModel.swift */, + E13DD3F82717E961009D4DAF /* UserListViewModel.swift */, + E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */, 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, 625CB57B2678CE1000530A6E /* ViewModel.swift */, - 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -680,32 +776,12 @@ 535870612669D21600D05A09 /* JellyfinPlayer tvOS */ = { isa = PBXGroup; children = ( - 5310694F2684E7EE00CFFDBA /* VideoPlayer */, - 536D3D77267BB9650004248C /* Components */, - 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */, - 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, - 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */, + E12186DF2718F2030010884C /* App */, 535870662669D21700D05A09 /* Assets.xcassets */, - 5358706B2669D21700D05A09 /* PersistenceController.swift */, + 536D3D77267BB9650004248C /* Components */, 535870702669D21700D05A09 /* Info.plist */, 535870682669D21700D05A09 /* Preview Content */, - 53ABFDDD267974E300886593 /* SplashView.swift */, - 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */, - 531690EE267ABF72005D8AB9 /* NextUpView.swift */, - 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, - 531690E4267ABD5C005D8AB9 /* MainTabView.swift */, - 531690E6267ABD79005D8AB9 /* HomeView.swift */, - 531690F8267AD135005D8AB9 /* README.md */, - 536D3D7E267BDF100004248C /* LatestMediaView.swift */, - 53A83C32268A309300DF3D92 /* LibraryView.swift */, - C4E508172703E8190045C9AB /* LibraryListView.swift */, - C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, - 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, - 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, - 53116A16268B919A003024C9 /* SeriesItemView.swift */, - 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, - 53272538268C20100035FBF1 /* EpisodeItemView.swift */, - 5398514426B64DA100101B49 /* SettingsView.swift */, + E12186E02718F23B0010884C /* Views */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -722,12 +798,14 @@ isa = PBXGroup; children = ( 6286F09F271C0AA500C40ED5 /* Generated */, + 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, E1FCD08E26C466F3007C8DCF /* Errors */, 621338912660106C00A81A2A /* Extensions */, 535870AB2669D8D300D05A09 /* Objects */, AE8C3157265D6F5E008AA076 /* Resources */, - 091B5A852683142E00D78B61 /* ServerLocator */, + 091B5A852683142E00D78B61 /* ServerDiscovery */, 62EC352A26766657000E9F2D /* Singleton */, + E13DD3C0271648EC009D4DAF /* SwiftfinStore */, 532175392671BCED005491E6 /* ViewModels */, E1AD105326D96F5A003E4A08 /* Views */, ); @@ -737,10 +815,16 @@ 535870AB2669D8D300D05A09 /* Objects */ = { isa = PBXGroup; children = ( - 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, - 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, - 535870AC2669D8DD00D05A09 /* Typings.swift */, + E1D4BF802719D22800A11E64 /* AppAppearance.swift */, + E1D4BF862719D27100A11E64 /* Bitrates.swift */, E1AD104926D94822003E4A08 /* DetailItem.swift */, + 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, + 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + E19169CD272514760085832A /* HTTPScheme.swift */, + E193D4DA27193CCA00900D82 /* PillStackable.swift */, + E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */, + E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, + 535870AC2669D8DD00D05A09 /* Typings.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, ); path = Objects; @@ -750,11 +834,11 @@ isa = PBXGroup; children = ( 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, + E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, + 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, + 53116A18268B947A003024C9 /* PlainLinkButton.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, 536D3D87267C17350004248C /* PublicUserButton.swift */, - 53116A18268B947A003024C9 /* PlainLinkButton.swift */, - 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, - E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, ); path = Components; sourceTree = ""; @@ -786,35 +870,15 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( - 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, - 53F866422687A45400DCD1D7 /* Components */, - 62ECA01926FA6D6900E8EBB7 /* Singleton */, - 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, + E1DD1127271E7D15005BE12F /* Objects */, + E13DD3BB27163C3E009D4DAF /* App */, + 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, - 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, - 5389276D263C25100035E14B /* ContinueWatchingView.swift */, + 53F866422687A45400DCD1D7 /* Components */, 5377CC02263B596B003A4E83 /* Info.plist */, - E14F7D0A26DB3714007C3AE6 /* ItemView */, - 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, - 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, - 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, - 6213388F265F83A900A81A2A /* LibraryListView.swift */, - 53EE24E5265060780068F029 /* LibrarySearchView.swift */, - 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, - 53892771263C8C6F0035E14B /* LoadingView.swift */, - 5389276F263C25230035E14B /* NextUpView.swift */, - 5377CBFD263B596B003A4E83 /* PersistenceController.swift */, + 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBFA263B596B003A4E83 /* Preview Content */, - 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, - E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, - 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, - 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */, - 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, - 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, - 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, - 625CB5672678B6FB00530A6E /* SplashView.swift */, - 625CB56E2678C23300530A6E /* HomeView.swift */, - 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */, + E13DD3D027165886009D4DAF /* Views */, ); path = JellyfinPlayer; sourceTree = ""; @@ -982,6 +1046,7 @@ E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */, + C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */, ); path = Components; sourceTree = ""; @@ -992,10 +1057,10 @@ 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, - 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, 621338922660107500A81A2A /* StringExtensions.swift */, + E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */, 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, 6286F0A5271C0EB700C40ED5 /* R.swift+SwiftUI.swift */, ); @@ -1013,7 +1078,6 @@ 628B95252670CABD0091AF3B /* WidgetExtension */ = { isa = PBXGroup; children = ( - 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */, 628B95362670CB800091AF3B /* JellyfinWidget.swift */, 628B95262670CABD0091AF3B /* NextUpWidget.swift */, 628B95282670CABE0091AF3B /* Assets.xcassets */, @@ -1026,15 +1090,20 @@ isa = PBXGroup; children = ( 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, + E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */, 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, + E193D5412719404B00900D82 /* MainCoordinator */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, - 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */, - 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */, + C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, + C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, + E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, + E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */, + E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */, ); path = Coordinators; @@ -1045,25 +1114,25 @@ children = ( 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, 62EC352E267666A5000E9F2D /* SessionManager.swift */, + E13DD3EE27178F87009D4DAF /* SwiftfinNotificationCenter.swift */, ); path = Singleton; sourceTree = ""; }; - 62ECA01926FA6D6900E8EBB7 /* Singleton */ = { + 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */ = { isa = PBXGroup; children = ( 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */, + 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */, ); - path = Singleton; + path = AppURLHandler; sourceTree = ""; }; AE8C3157265D6F5E008AA076 /* Resources */ = { isa = PBXGroup; children = ( AE8C3158265D6F90008AA076 /* bitrates.json */, - 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */, ); path = Resources; sourceTree = ""; @@ -1079,6 +1148,85 @@ path = Pods; sourceTree = ""; }; + E12186DF2718F2030010884C /* App */ = { + isa = PBXGroup; + children = ( + 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */, + ); + path = App; + sourceTree = ""; + }; + E12186E02718F23B0010884C /* Views */ = { + isa = PBXGroup; + children = ( + 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, + 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */, + 531690E6267ABD79005D8AB9 /* HomeView.swift */, + E193D54E271942C000900D82 /* ItemView */, + 536D3D7E267BDF100004248C /* LatestMediaView.swift */, + E193D54C2719426600900D82 /* LibraryFilterView.swift */, + C4E508172703E8190045C9AB /* LibraryListView.swift */, + C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, + 53A83C32268A309300DF3D92 /* LibraryView.swift */, + C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, + C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, + 531690EE267ABF72005D8AB9 /* NextUpView.swift */, + E193D54F2719430400900D82 /* ServerDetailView.swift */, + E193D54A271941D300900D82 /* ServerListView.swift */, + 5398514426B64DA100101B49 /* SettingsView.swift */, + E193D546271941C500900D82 /* UserListView.swift */, + E193D548271941CC00900D82 /* UserSignInView.swift */, + 5310694F2684E7EE00CFFDBA /* VideoPlayer */, + ); + path = Views; + sourceTree = ""; + }; + E13DD3BB27163C3E009D4DAF /* App */ = { + isa = PBXGroup; + children = ( + E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */, + E13DD3BC27163C63009D4DAF /* EmailHelper.swift */, + 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, + E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */, + ); + path = App; + sourceTree = ""; + }; + E13DD3C0271648EC009D4DAF /* SwiftfinStore */ = { + isa = PBXGroup; + children = ( + E13DD3C127164941009D4DAF /* SwiftfinStore.swift */, + E13DD3D4271693CD009D4DAF /* SwiftfinStoreDefaults.swift */, + ); + path = SwiftfinStore; + sourceTree = ""; + }; + E13DD3D027165886009D4DAF /* Views */ = { + isa = PBXGroup; + children = ( + E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */, + 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, + 5389276D263C25100035E14B /* ContinueWatchingView.swift */, + 625CB56E2678C23300530A6E /* HomeView.swift */, + E14F7D0A26DB3714007C3AE6 /* ItemView */, + 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, + 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, + 6213388F265F83A900A81A2A /* LibraryListView.swift */, + 53EE24E5265060780068F029 /* LibrarySearchView.swift */, + 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, + 53892771263C8C6F0035E14B /* LoadingView.swift */, + 5389276F263C25230035E14B /* NextUpView.swift */, + E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, + E13DD3E427177D15009D4DAF /* ServerListView.swift */, + 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, + E13DD3FB2717EAE8009D4DAF /* UserListView.swift */, + E13DD3F4271793BB009D4DAF /* UserSignInView.swift */, + E193D5452719418B00900D82 /* VideoPlayer */, + ); + path = Views; + sourceTree = ""; + }; E14F7D0A26DB3714007C3AE6 /* ItemView */ = { isa = PBXGroup; children = ( @@ -1108,12 +1256,48 @@ path = Landscape; sourceTree = ""; }; + E193D5412719404B00900D82 /* MainCoordinator */ = { + isa = PBXGroup; + children = ( + 62C29E9E26D1016600C1D2E7 /* iOSMainCoordinator.swift */, + 62C29EA026D102A500C1D2E7 /* iOSMainTabCoordinator.swift */, + E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */, + E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */, + ); + path = MainCoordinator; + sourceTree = ""; + }; + E193D5452719418B00900D82 /* VideoPlayer */ = { + isa = PBXGroup; + children = ( + 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, + 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, + 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, + 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, + 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */, + ); + path = VideoPlayer; + sourceTree = ""; + }; + E193D54E271942C000900D82 /* ItemView */ = { + isa = PBXGroup; + children = ( + 53272538268C20100035FBF1 /* EpisodeItemView.swift */, + 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, + 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, + 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, + 53116A16268B919A003024C9 /* SeriesItemView.swift */, + ); + path = ItemView; + sourceTree = ""; + }; E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( - 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, - E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */, + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, + 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, + E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, ); path = JellyfinAPIExtensions; @@ -1122,6 +1306,7 @@ E1AD105326D96F5A003E4A08 /* Views */ = { isa = PBXGroup; children = ( + 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, 621338B22660A07800A81A2A /* LazyView.swift */, 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, @@ -1132,6 +1317,14 @@ path = Views; sourceTree = ""; }; + E1DD1127271E7D15005BE12F /* Objects */ = { + isa = PBXGroup; + children = ( + E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */, + ); + path = Objects; + sourceTree = ""; + }; E1FCD08E26C466F3007C8DCF /* Errors */ = { isa = PBXGroup; children = ( @@ -1162,18 +1355,17 @@ ); name = "JellyfinPlayer tvOS"; packageProductDependencies = ( - 5358708C2669D7A800D05A09 /* KeychainSwift */, 535870902669D7A800D05A09 /* Introspect */, - 5358709A2669D7A800D05A09 /* NukeUI */, 53A431BE266B0FFE0016769F /* JellyfinAPI */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, 536D3D83267BEA550004248C /* ParallaxView */, - 62CB3F472685BB3B003D0A6F /* Defaults */, - 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */, 53649AAE269CFAF600A2D8B7 /* Puppy */, - 6261A0DF26A0AB710072EF1C /* CombineExt */, 6220D0C826D63F3700B8E046 /* Stinsen */, - C49FB6582717A06300AAEABB /* SwiftUICollection */, + E13DD3CC27164CA7009D4DAF /* CoreStore */, + E12186DD2718F1C50010884C /* Defaults */, + E1218C9D271A2CD600EA0737 /* CombineExt */, + E1218C9F271A2CF200EA0737 /* Nuke */, + E1A9999A271A343C008E78C0 /* SwiftUICollection */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -1200,17 +1392,17 @@ ); name = "JellyfinPlayer iOS"; packageProductDependencies = ( - 5338F756263B7E2E0014BF09 /* KeychainSwift */, 53352570265EA0A0006CCA86 /* Introspect */, - 621C637F26672A30004216EA /* NukeUI */, 53A431BC266B0FF20016769F /* JellyfinAPI */, 625CB5792678C4A400530A6E /* ActivityIndicator */, - 53EC6E24267EB10F006DD26A /* SwiftyJSON */, - 62CB3F452685BAF7003D0A6F /* Defaults */, 53649AAC269CFAEA00A2D8B7 /* Puppy */, - 6260FFF826A09754003FA968 /* CombineExt */, 62C29E9B26D0FE4200C1D2E7 /* Stinsen */, - C4BFD4E427167B63007739E3 /* SwiftUICollection */, + E13DD3C52716499E009D4DAF /* CoreStore */, + E13DD3D227168E65009D4DAF /* Defaults */, + E1B6DCE7271A23780015B715 /* CombineExt */, + E1B6DCE9271A23880015B715 /* SwiftyJSON */, + E1218C99271A26BA00EA0737 /* Nuke */, + E1A99998271A3429008E78C0 /* SwiftUICollection */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -1230,12 +1422,12 @@ ); name = WidgetExtension; packageProductDependencies = ( - 628B95322670CAEA0091AF3B /* NukeUI */, 628B95342670CAEA0091AF3B /* JellyfinAPI */, - 628B95392670CE250091AF3B /* KeychainSwift */, 536D3D7C267BD5F90004248C /* ActivityIndicator */, 53649AB4269D423A00A2D8B7 /* Puppy */, - 53628C6C26B5AA0D008A64A0 /* Defaults */, + E13DD3CE27164E1F009D4DAF /* CoreStore */, + E13DD3DC27175CE3009D4DAF /* Defaults */, + E1218C9B271A26C400EA0737 /* Nuke */, ); productName = WidgetExtensionExtension; productReference = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; @@ -1288,18 +1480,17 @@ ); mainGroup = 5377CBE8263B596A003A4E83; packageReferences = ( - 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */, 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, - 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */, 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, - 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */, - 53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */, 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */, - 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, + E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, + E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, + E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */, + E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */, C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; @@ -1537,20 +1728,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, 531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, + E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, + E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, + C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, 531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, - 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */, + E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, - 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, - 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, + E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, + E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, + C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, + E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, + E193D53E27193F9A00900D82 /* VideoPlayerCoordinator.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, @@ -1560,9 +1758,11 @@ 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, + E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */, E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */, + E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1572,57 +1772,80 @@ 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, + E193D54B271941D300900D82 /* ServerListView.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, + E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, + E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, + E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, + E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, + E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 6286F0A2271C0AA500C40ED5 /* R.generated.swift in Sources */, + E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, + E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, + E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, + E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, - 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, + E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */, + E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, - 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, - 62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, - 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, + E13DD3D6271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, + E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, + E19169CF272514760085832A /* HTTPScheme.swift in Sources */, C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, 531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */, - 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, + E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, + C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, + E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, + E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, + E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, 536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */, + E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */, 5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */, C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */, + E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, + E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, + E193D547271941C500900D82 /* UserListView.swift in Sources */, + E1D4BF7F2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, + E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, - 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, + C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, - E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */, - 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, + C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, + E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, + E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1633,32 +1856,36 @@ 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, + E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, + E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, - 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, - 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */, - 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */, + 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, - 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */, + 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, + C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, + C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, + E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, - 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, + E19169CE272514760085832A /* HTTPScheme.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */, @@ -1668,7 +1895,7 @@ 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, - 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */, + C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, @@ -1677,50 +1904,74 @@ 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, - 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, + C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6286F0A1271C0AA500C40ED5 /* R.generated.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, + E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, + E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, - 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, + E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, + E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, + E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, + E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, + E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 6286F0A6271C0EB700C40ED5 /* R.swift+SwiftUI.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, + E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, + E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */, + E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, + E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */, + E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */, 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, + E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, + C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, + C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, + E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, + E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, + E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, + C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, + E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, - 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, + E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, + E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, + E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, + E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, + C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, + E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, @@ -1733,22 +1984,27 @@ buildActionMask = 2147483647; files = ( 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */, - 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */, + E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */, + E19169D0272514760085832A /* HTTPScheme.swift in Sources */, 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */, + E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */, 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */, - 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */, E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */, 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, + E13DD3F72717E87D009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, + E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */, 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */, - 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, + E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */, E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */, + E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, + E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1898,14 +2154,13 @@ isa = XCBuildConfiguration; baseConfigurationReference = DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_APPICON_NAME = "Dev App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1914,7 +2169,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -1932,11 +2187,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1945,7 +2199,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; @@ -2081,14 +2335,14 @@ baseConfigurationReference = 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Dev"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2100,7 +2354,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2122,10 +2376,10 @@ CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2137,7 +2391,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2153,19 +2407,18 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; - DEVELOPMENT_TEAM = 9R8RREG67J; + CURRENT_PROJECT_VERSION = 66; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2180,19 +2433,18 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; - DEVELOPMENT_TEAM = 9R8RREG67J; + CURRENT_PROJECT_VERSION = 66; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -2243,14 +2495,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/rmnblm/SwiftUIFocusGuide"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.0; - }; - }; 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; @@ -2259,14 +2503,6 @@ minimumVersion = 0.1.3; }; }; - 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/evgenyneu/keychain-swift"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 19.0.0; - }; - }; 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sushichop/Puppy"; @@ -2291,22 +2527,6 @@ kind = branch; }; }; - 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; - requirement = { - branch = master; - kind = branch; - }; - }; - 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kean/NukeUI"; - requirement = { - kind = exactVersion; - version = 0.3.0; - }; - }; 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duyquang91/ActivityIndicator"; @@ -2315,14 +2535,6 @@ minimumVersion = 1.1.0; }; }; - 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/acvigue/CombineExt"; - requirement = { - branch = main; - kind = branch; - }; - }; 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/rundfunk47/stinsen"; @@ -2331,14 +2543,6 @@ minimumVersion = 2.0.2; }; }; - 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/acvigue/Defaults"; - requirement = { - branch = main; - kind = branch; - }; - }; C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ABJC/SwiftUICollection"; @@ -2347,44 +2551,59 @@ kind = branch; }; }; + E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kean/Nuke"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; + }; + }; + E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/CombineCommunity/CombineExt"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.1.0; + }; + }; + E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.0; + }; + }; + E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */ = { - isa = XCSwiftPackageProductDependency; - package = 53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */; - productName = SwiftUIFocusGuide; - }; 53352570265EA0A0006CCA86 /* Introspect */ = { isa = XCSwiftPackageProductDependency; package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; - 5338F756263B7E2E0014BF09 /* KeychainSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; - productName = KeychainSwift; - }; - 5358708C2669D7A800D05A09 /* KeychainSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; - productName = KeychainSwift; - }; 535870902669D7A800D05A09 /* Introspect */ = { isa = XCSwiftPackageProductDependency; package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; - 5358709A2669D7A800D05A09 /* NukeUI */ = { - isa = XCSwiftPackageProductDependency; - package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; - productName = NukeUI; - }; - 53628C6C26B5AA0D008A64A0 /* Defaults */ = { - isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; - productName = Defaults; - }; 53649AAC269CFAEA00A2D8B7 /* Puppy */ = { isa = XCSwiftPackageProductDependency; package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */; @@ -2425,16 +2644,6 @@ package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; productName = ActivityIndicator; }; - 53EC6E24267EB10F006DD26A /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; - 621C637F26672A30004216EA /* NukeUI */ = { - isa = XCSwiftPackageProductDependency; - package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; - productName = NukeUI; - }; 6220D0C826D63F3700B8E046 /* Stinsen */ = { isa = XCSwiftPackageProductDependency; package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; @@ -2445,70 +2654,87 @@ package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; productName = ActivityIndicator; }; - 6260FFF826A09754003FA968 /* CombineExt */ = { - isa = XCSwiftPackageProductDependency; - package = 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */; - productName = CombineExt; - }; - 6261A0DF26A0AB710072EF1C /* CombineExt */ = { - isa = XCSwiftPackageProductDependency; - package = 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */; - productName = CombineExt; - }; - 628B95322670CAEA0091AF3B /* NukeUI */ = { - isa = XCSwiftPackageProductDependency; - package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; - productName = NukeUI; - }; 628B95342670CAEA0091AF3B /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; productName = JellyfinAPI; }; - 628B95392670CE250091AF3B /* KeychainSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; - productName = KeychainSwift; - }; 62C29E9B26D0FE4200C1D2E7 /* Stinsen */ = { isa = XCSwiftPackageProductDependency; package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; productName = Stinsen; }; - 62CB3F452685BAF7003D0A6F /* Defaults */ = { + E12186DD2718F1C50010884C /* Defaults */ = { isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; - 62CB3F472685BB3B003D0A6F /* Defaults */ = { + E1218C99271A26BA00EA0737 /* Nuke */ = { isa = XCSwiftPackageProductDependency; - package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = Nuke; + }; + E1218C9B271A26C400EA0737 /* Nuke */ = { + isa = XCSwiftPackageProductDependency; + package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = Nuke; + }; + E1218C9D271A2CD600EA0737 /* CombineExt */ = { + isa = XCSwiftPackageProductDependency; + package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; + productName = CombineExt; + }; + E1218C9F271A2CF200EA0737 /* Nuke */ = { + isa = XCSwiftPackageProductDependency; + package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = Nuke; + }; + E13DD3C52716499E009D4DAF /* CoreStore */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; + productName = CoreStore; + }; + E13DD3CC27164CA7009D4DAF /* CoreStore */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; + productName = CoreStore; + }; + E13DD3CE27164E1F009D4DAF /* CoreStore */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; + productName = CoreStore; + }; + E13DD3D227168E65009D4DAF /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; - C49FB6582717A06300AAEABB /* SwiftUICollection */ = { + E13DD3DC27175CE3009D4DAF /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + E1A99998271A3429008E78C0 /* SwiftUICollection */ = { isa = XCSwiftPackageProductDependency; package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; productName = SwiftUICollection; }; - C4BFD4E427167B63007739E3 /* SwiftUICollection */ = { + E1A9999A271A343C008E78C0 /* SwiftUICollection */ = { isa = XCSwiftPackageProductDependency; package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; productName = SwiftUICollection; }; + E1B6DCE7271A23780015B715 /* CombineExt */ = { + isa = XCSwiftPackageProductDependency; + package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; + productName = CombineExt; + }; + E1B6DCE9271A23880015B715 /* SwiftyJSON */ = { + isa = XCSwiftPackageProductDependency; + package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; + productName = SwiftyJSON; + }; /* End XCSwiftPackageProductDependency section */ - -/* Begin XCVersionGroup section */ - 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - 5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */, - ); - currentVersion = 5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */; - path = Model.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ }; rootObject = 5377CBE9263B596A003A4E83 /* Project object */; } diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 955f206f..5378c461 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -20,39 +20,30 @@ } }, { - "package": "combine-schedulers", - "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "package": "CombineExt", + "repositoryURL": "https://github.com/CombineCommunity/CombineExt", "state": { "branch": null, - "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", - "version": "0.5.3" + "revision": "0880829102152185190064fd17847a7c681d2127", + "version": "1.5.1" } }, { - "package": "CombineExt", - "repositoryURL": "https://github.com/acvigue/CombineExt", + "package": "CoreStore", + "repositoryURL": "https://github.com/JohnEstropia/CoreStore.git", "state": { - "branch": "main", - "revision": "f629c5b052d1cb5d03e10890deccc50e4c649e68", - "version": null + "branch": null, + "revision": "496145761ab30e8cf1c44220c0882b95e6b41077", + "version": "8.1.0" } }, { "package": "Defaults", - "repositoryURL": "https://github.com/acvigue/Defaults", - "state": { - "branch": "main", - "revision": "a4153b523ab3df9f5e3f70e9cfe9c54bed98c7e3", - "version": null - } - }, - { - "package": "Gifu", - "repositoryURL": "https://github.com/kaishin/Gifu", + "repositoryURL": "https://github.com/sindresorhus/Defaults", "state": { "branch": null, - "revision": "51f2eab32903e336f590c013267cfa4d7f8b06c4", - "version": "3.3.1" + "revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a", + "version": "6.1.0" } }, { @@ -64,31 +55,13 @@ "version": null } }, - { - "package": "KeychainSwift", - "repositoryURL": "https://github.com/evgenyneu/keychain-swift", - "state": { - "branch": null, - "revision": "96fb84f45a96630e7583903bd7e08cf095c7a7ef", - "version": "19.0.0" - } - }, { "package": "Nuke", - "repositoryURL": "https://github.com/kean/Nuke.git", + "repositoryURL": "https://github.com/kean/Nuke", "state": { "branch": null, - "revision": "0db18dd34998cca18e9a28bcee136f84518007a0", - "version": "10.4.1" - } - }, - { - "package": "NukeUI", - "repositoryURL": "https://github.com/kean/NukeUI", - "state": { - "branch": null, - "revision": "d2580b8d22b29c6244418d8e4b568f3162191460", - "version": "0.3.0" + "revision": "7f73ceaeacd5df75a7994cd82e165ad9ff1815db", + "version": "9.6.1" } }, { @@ -145,31 +118,13 @@ "version": null } }, - { - "package": "SwiftUIFocusGuide", - "repositoryURL": "https://github.com/rmnblm/SwiftUIFocusGuide", - "state": { - "branch": null, - "revision": "fb8eefaccb2954efedc19a5539241f370baa4a10", - "version": "0.1.0" - } - }, { "package": "SwiftyJSON", "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON", - "state": { - "branch": "master", - "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version": null - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", - "version": "0.2.1" + "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", + "version": "5.0.1" } } ] diff --git a/JellyfinPlayer/App/AppDelegate.swift b/JellyfinPlayer/App/AppDelegate.swift new file mode 100644 index 00000000..d41c406f --- /dev/null +++ b/JellyfinPlayer/App/AppDelegate.swift @@ -0,0 +1,27 @@ +// + /* + * 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 UIKit + +class AppDelegate: NSObject, UIApplicationDelegate { + static var orientationLock = UIInterfaceOrientationMask.all + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + + // Lazily initialize datastack + let _ = SwiftfinStore.dataStack + + return true + } + + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + AppDelegate.orientationLock + } +} diff --git a/JellyfinPlayer/App/EmailHelper.swift b/JellyfinPlayer/App/EmailHelper.swift new file mode 100644 index 00000000..5c054d9f --- /dev/null +++ b/JellyfinPlayer/App/EmailHelper.swift @@ -0,0 +1,82 @@ +// + /* + * 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 MessageUI + +class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { + + public static let shared = EmailHelper() + + override private init() { } + + func sendLogs(logURL: URL) { + if !MFMailComposeViewController.canSendMail() { + // Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account") + return // EXIT + } + + let picker = MFMailComposeViewController() + + let fileManager = FileManager() + let data = fileManager.contents(atPath: logURL.path) + + picker.setSubject("[DEV-BUG] SwiftFin") + picker + .setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n", + isHTML: false) + picker.setToRecipients(["SwiftFin Bug Reports "]) + picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent) + picker.mailComposeDelegate = self + + EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil) + } + + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil) + } + + static func getRootViewController() -> UIViewController? { + UIApplication.shared.windows.first?.rootViewController + } +} + +// A view modifier that detects shaking and calls a function of our choosing. +struct DeviceShakeViewModifier: ViewModifier { + let action: () -> Void + + func body(content: Self.Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in + action() + } + } +} + +// A View extension to make the modifier easier to use. +extension View { + func onShake(perform action: @escaping () -> Void) -> some View { + modifier(DeviceShakeViewModifier(action: action)) + } +} + +// The notification we'll send when a shake gesture happens. +extension UIDevice { + static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification") +} + +// Override the default behavior of shake gestures to send our notification instead. +extension UIWindow { + override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + if motion == .motionShake { + NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) + } + } +} diff --git a/JellyfinPlayer/App/JellyfinPlayerApp.swift b/JellyfinPlayer/App/JellyfinPlayerApp.swift new file mode 100644 index 00000000..27440a56 --- /dev/null +++ b/JellyfinPlayer/App/JellyfinPlayerApp.swift @@ -0,0 +1,63 @@ +/* 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 Defaults +import MessageUI +import Stinsen +import SwiftUI + +// MARK: JellyfinPlayerApp +@main +struct JellyfinPlayerApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @Default(.appAppearance) var appAppearance + + var body: some Scene { + WindowGroup { + EmptyView() + .ignoresSafeArea() + .onAppear { + setupAppearance() + } + .withHostingWindow { window in + window?.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view()) + } + .onShake { + EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) + } + .onOpenURL { url in + AppURLHandler.shared.processDeepLink(url: url) + } + } + } + + private func setupAppearance() { + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style + } +} + +// MARK: Hosting Window +struct HostingWindowFinder: UIViewRepresentable { + var callback: (UIWindow?) -> Void + + func makeUIView(context: Context) -> UIView { + let view = UIView() + DispatchQueue.main.async { [weak view] in + callback(view?.window) + } + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} +} + +extension View { + func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { + background(HostingWindowFinder(callback: callback)) + } +} diff --git a/JellyfinPlayer/App/PreferenceUIHostingController.swift b/JellyfinPlayer/App/PreferenceUIHostingController.swift new file mode 100644 index 00000000..d6146c75 --- /dev/null +++ b/JellyfinPlayer/App/PreferenceUIHostingController.swift @@ -0,0 +1,118 @@ +// + /* + * 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 UIKit +import SwiftUI + +// MARK: PreferenceUIHostingController +class PreferenceUIHostingController: UIHostingController { + init(wrappedView: V) { + let box = Box() + super.init(rootView: AnyView(wrappedView + .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { + box.value?._prefersHomeIndicatorAutoHidden = $0 + }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { + box.value?._orientations = $0 + }.onPreferenceChange(ViewPreferenceKey.self) { + box.value?._viewPreference = $0 + })) + box.value = self + } + + @objc dynamic required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + super.modalPresentationStyle = .fullScreen + } + + private class Box { + weak var value: PreferenceUIHostingController? + init() {} + } + + // MARK: Prefers Home Indicator Auto Hidden + + public var _prefersHomeIndicatorAutoHidden = false { + didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } + } + + override var prefersHomeIndicatorAutoHidden: Bool { + _prefersHomeIndicatorAutoHidden + } + + // MARK: Lock orientation + + public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown { + didSet { + if _orientations == .landscape { + let value = UIInterfaceOrientation.landscapeRight.rawValue + UIDevice.current.setValue(value, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } + } + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + _orientations + } + + public var _viewPreference: UIUserInterfaceStyle = .unspecified { + didSet { + overrideUserInterfaceStyle = _viewPreference + } + } +} + +// MARK: Preference Keys +struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey { + typealias Value = Bool + + static var defaultValue: Value = false + + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() || value + } +} + +struct ViewPreferenceKey: PreferenceKey { + typealias Value = UIUserInterfaceStyle + + static var defaultValue: UIUserInterfaceStyle = .unspecified + + static func reduce(value: inout UIUserInterfaceStyle, nextValue: () -> UIUserInterfaceStyle) { + value = nextValue() + } +} + +struct SupportedOrientationsPreferenceKey: PreferenceKey { + typealias Value = UIInterfaceOrientationMask + static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown + + static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { + // use the most restrictive set from the stack + value.formIntersection(nextValue()) + } +} + +// MARK: Preference Key View Extension +extension View { + // Controls the application's preferred home indicator auto-hiding when this view is shown. + func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View { + preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value) + } + + func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View { + // When rendered, export the requested orientations upward to Root + preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations) + } + + func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View { + // When rendered, export the requested orientations upward to Root + preference(key: ViewPreferenceKey.self, value: viewPreference) + } +} diff --git a/JellyfinPlayer/Singleton/AppURLHandler.swift b/JellyfinPlayer/AppURLHandler/AppURLHandler.swift similarity index 95% rename from JellyfinPlayer/Singleton/AppURLHandler.swift rename to JellyfinPlayer/AppURLHandler/AppURLHandler.swift index 9dcb5eaf..4c64b916 100644 --- a/JellyfinPlayer/Singleton/AppURLHandler.swift +++ b/JellyfinPlayer/AppURLHandler/AppURLHandler.swift @@ -82,7 +82,7 @@ extension AppURLHandler { // It would be nice if the ItemViewModel could be initialized to id later. getItem(userID: userID, itemID: itemID) { item in guard let item = item else { return } - NotificationCenter.default.post(name: Notification.Name("processDeepLink"), object: DeepLink.item(item)) + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.processDeepLink, object: DeepLink.item(item)) } return true diff --git a/JellyfinPlayer/DeepLink.swift b/JellyfinPlayer/AppURLHandler/DeepLink.swift similarity index 100% rename from JellyfinPlayer/DeepLink.swift rename to JellyfinPlayer/AppURLHandler/DeepLink.swift diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/100.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/100.png new file mode 100755 index 00000000..5f412bcd Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/100.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/1024.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/1024.png new file mode 100755 index 00000000..74aea638 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/1024.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/114.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/114.png new file mode 100755 index 00000000..b4caaaa1 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/114.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/120.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/120.png new file mode 100755 index 00000000..ac85a191 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/120.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/144.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/144.png new file mode 100755 index 00000000..795d39ca Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/144.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/152.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/152.png new file mode 100755 index 00000000..8ba00881 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/152.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/167.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/167.png new file mode 100755 index 00000000..d23e2dbd Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/167.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/180.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/180.png new file mode 100755 index 00000000..71d6a10d Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/180.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/20.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/20.png new file mode 100755 index 00000000..53baa4a8 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/20.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/29.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/29.png new file mode 100755 index 00000000..e9f48888 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/29.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/40.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/40.png new file mode 100755 index 00000000..c60b692c Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/40.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/50.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/50.png new file mode 100755 index 00000000..e6991944 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/50.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/57.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/57.png new file mode 100755 index 00000000..57b02361 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/57.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/58.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/58.png new file mode 100755 index 00000000..94956434 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/58.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/60.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/60.png new file mode 100755 index 00000000..5f466f04 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/60.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/72.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/72.png new file mode 100755 index 00000000..dad26e7d Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/72.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/76.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/76.png new file mode 100755 index 00000000..007e2616 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/76.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/80.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/80.png new file mode 100755 index 00000000..9f9bc982 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/80.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/87.png b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/87.png new file mode 100755 index 00000000..a50c9440 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/87.png differ diff --git a/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json new file mode 100755 index 00000000..65b74d7e --- /dev/null +++ b/JellyfinPlayer/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} \ No newline at end of file diff --git a/JellyfinPlayer/Assets.xcassets/LaunchScreenBackground.colorset/Contents.json b/JellyfinPlayer/Assets.xcassets/LaunchScreenBackground.colorset/Contents.json new file mode 100644 index 00000000..04256378 --- /dev/null +++ b/JellyfinPlayer/Assets.xcassets/LaunchScreenBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/Contents.json b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/Contents.json new file mode 100644 index 00000000..e708d061 --- /dev/null +++ b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "swiftfin-logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "swiftfin-logo-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "swiftfin-logo-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-1.png b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-1.png new file mode 100644 index 00000000..efdfe428 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-1.png differ diff --git a/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-2.png b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-2.png new file mode 100644 index 00000000..efdfe428 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo-2.png differ diff --git a/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo.png b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo.png new file mode 100644 index 00000000..efdfe428 Binary files /dev/null and b/JellyfinPlayer/Assets.xcassets/swiftfin-logo.imageset/swiftfin-logo.png differ diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift index e0b5d0fc..d9ce7769 100644 --- a/JellyfinPlayer/Components/PillHStackView.swift +++ b/JellyfinPlayer/Components/PillHStackView.swift @@ -9,10 +9,6 @@ import SwiftUI -protocol PillStackable { - var title: String { get } -} - struct PillHStackView: View { let title: String diff --git a/JellyfinPlayer/Components/PortraitHStackView.swift b/JellyfinPlayer/Components/PortraitHStackView.swift index 4d97d34a..db9febec 100644 --- a/JellyfinPlayer/Components/PortraitHStackView.swift +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -9,14 +9,6 @@ import SwiftUI -public protocol PortraitImageStackable { - func imageURLContsructor(maxWidth: Int) -> URL - var title: String { get } - var description: String? { get } - var blurHash: String { get } - var failureInitials: String { get } -} - struct PortraitImageHStackView: View { let items: [ItemType] diff --git a/JellyfinPlayer/Components/PortraitItemElement.swift b/JellyfinPlayer/Components/PortraitItemElement.swift new file mode 100644 index 00000000..9a63b6f1 --- /dev/null +++ b/JellyfinPlayer/Components/PortraitItemElement.swift @@ -0,0 +1,20 @@ +// + /* + * 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 JellyfinAPI + +// Not implemented on iOS, but used by a shared Coordinator. +struct PortraitItemElement: View { + var item: BaseItemDto + + var body: some View { + EmptyView() + } +} diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift deleted file mode 100644 index 15f6fe04..00000000 --- a/JellyfinPlayer/ConnectToServerView.swift +++ /dev/null @@ -1,184 +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 Stinsen - -struct ConnectToServerView: View { - @EnvironmentObject var mainRouter: MainCoordinator.Router - @StateObject var viewModel = ConnectToServerViewModel() - @State var username = "" - @State var password = "" - @State var uri = "" - - var body: some View { - ZStack { - Form { - if viewModel.isConnectedServer { - if viewModel.publicUsers.isEmpty { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { - TextField(NSLocalizedString("Username", comment: ""), text: $username) - .disableAutocorrection(true) - .autocapitalization(.none) - SecureField(NSLocalizedString("Password", comment: ""), text: $password) - .disableAutocorrection(true) - .autocapitalization(.none) - Button { - viewModel.login() - } label: { - HStack { - Text("Login") - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - }.disabled(viewModel.isLoading || username.isEmpty) - } - - Section { - Button { - viewModel.isConnectedServer = false - } label: { - HStack { - HStack { - Image(systemName: "chevron.left") - Text("Change Server") - } - Spacer() - } - } - } - } else { - Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) { - ForEach(viewModel.publicUsers, id: \.id) { publicUser in - HStack { - Button(action: { - if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { - let user = SessionManager.current.getSavedSession(userID: publicUser.id!) - SessionManager.current.loginWithSavedSession(user: user) - mainRouter.root(\.mainTab) - } else { - username = publicUser.name ?? "" - viewModel.selectedPublicUser = publicUser - viewModel.hidePublicUsers() - if !(publicUser.hasPassword ?? true) { - password = "" - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - 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=60&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 { - viewModel.publicUsers.removeAll() - 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("Connect Manually")) { - TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.URL) - Button { - viewModel.connectToServer() - } label: { - HStack { - Text("Connect") - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - } - .disabled(viewModel.isLoading || uri.isEmpty) - } - - Section(header: Text("Discovered Servers")) { - if self.viewModel.searching { - ProgressView() - } - ForEach(self.viewModel.servers, id: \.id) { server in - Button(action: { - viewModel.connectToServer(at: server.url) - }, label: { - HStack { - Text(server.name) - .font(.headline) - Text("• \(server.host)") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - if viewModel.isLoading { - ProgressView() - } - } - - }) - } - } - .onAppear(perform: self.viewModel.discoverServers) - } - } - } - .onChange(of: uri) { uri in - viewModel.uriSubject.send(uri) - } - .onChange(of: username) { username in - viewModel.usernameSubject.send(username) - } - .onChange(of: password) { password in - viewModel.passwordSubject.send(password) - } - .alert(item: $viewModel.errorMessage) { _ in - Alert(title: Text("\(viewModel.errorMessage?.code ?? -1)\n\(viewModel.errorMessage?.title ?? "Error")"), - message: Text(viewModel.errorMessage?.displayMessage ?? "Error"), - dismissButton: .cancel()) - } - .navigationTitle(NSLocalizedString("Connect to Server", comment: "")) - .onAppear { - AppURLHandler.shared.appURLState = .allowedInLogin - } - } -} diff --git a/JellyfinPlayer/Coordinators/ItemCoordinator.swift b/JellyfinPlayer/Coordinators/ItemCoordinator.swift deleted file mode 100644 index 12781b52..00000000 --- a/JellyfinPlayer/Coordinators/ItemCoordinator.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -/* - * 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 -import Stinsen -import SwiftUI - -#if os(iOS) - final class ItemCoordinator: NavigationCoordinatable { - let stack = NavigationStack(initial: \ItemCoordinator.start) - - @Root var start = makeStart - @Route(.push) var item = makeItem - @Route(.push) var library = makeLibrary - @Route(.fullScreen) var videoPlayer = makeVideoPlayer - - let itemDto: BaseItemDto - - init(item: BaseItemDto) { - self.itemDto = item - } - - func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { - LibraryCoordinator(viewModel: params.viewModel, title: params.title) - } - - func makeItem(item: BaseItemDto) -> ItemCoordinator { - ItemCoordinator(item: item) - } - - func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) - } - - @ViewBuilder func makeStart() -> some View { - ItemNavigationView(item: itemDto) - } - } - -#elseif os(tvOS) - // temp for fixing build error - final class ItemCoordinator: NavigationCoordinatable { - let stack = NavigationStack(initial: \ItemCoordinator.start) - - @Root var start = makeStart - @Route(.push) var item = makeItem - @Route(.push) var library = makeLibrary - @Route(.fullScreen) var videoPlayer = makeVideoPlayer - - @ViewBuilder func makeStart() -> some View { - EmptyView() - } - - @ViewBuilder func makeLibrary(params: (viewModel: LibraryViewModel, title: String)) -> some View { - EmptyView() - } - - @ViewBuilder func makeItem(item: BaseItemDto) -> some View { - EmptyView() - } - - @ViewBuilder func makeVideoPlayer(item: BaseItemDto) -> some View { - EmptyView() - } - } -#endif diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift deleted file mode 100644 index 0d0f7f29..00000000 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -/* - * 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 Nuke -import Stinsen -import SwiftUI -#if !os(tvOS) - import WidgetKit -#endif - -#if os(iOS) - final class MainCoordinator: NavigationCoordinatable { - var stack: NavigationStack - - @Root var mainTab = makeMainTab - @Root var connectToServer = makeConnectToServer - - init() { - if ServerEnvironment.current.server != nil, SessionManager.current.user != nil { - self.stack = NavigationStack(initial: \MainCoordinator.mainTab) - } else { - self.stack = NavigationStack(initial: \MainCoordinator.connectToServer) - } - ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory - DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk - - #if !os(tvOS) - WidgetCenter.shared.reloadAllTimelines() - UIScrollView.appearance().keyboardDismissMode = .onDrag - #endif - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil) - nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil) - nc.addObserver(self, selector: #selector(processDeepLink), name: Notification.Name("processDeepLink"), object: nil) - } - - @objc func didLogIn() { - LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") - root(\.mainTab) - } - - @objc func didLogOut() { - LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") - root(\.connectToServer) - } - - @objc func processDeepLink(_ notification: Notification) { - guard let deepLink = notification.object as? DeepLink else { return } - if let coordinator = hasRoot(\.mainTab) { - switch deepLink { - case let .item(item): - coordinator.focusFirst(\.home) - .child - .popToRoot() - .route(to: \.item, item) - } - } - } - - func makeMainTab() -> MainTabCoordinator { - MainTabCoordinator() - } - - func makeConnectToServer() -> NavigationViewCoordinator { - NavigationViewCoordinator(ConnectToServerCoodinator()) - } - } - -#elseif os(tvOS) - // temp for fixing build error - final class MainCoordinator: NavigationCoordinatable { - var stack = NavigationStack(initial: \MainCoordinator.mainTab) - - @Root var mainTab = makeEmpty - - @ViewBuilder func makeEmpty() -> some View { - EmptyView() - } - } -#endif diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift deleted file mode 100644 index 05687d0e..00000000 --- a/JellyfinPlayer/HomeView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -/* - * 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 { - @EnvironmentObject var homeRouter: HomeCoordinator.Router - @StateObject var viewModel = HomeViewModel() - - init() { - let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") - let barAppearance = UINavigationBar.appearance() - barAppearance.backIndicatorImage = backButtonBackgroundImage - barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage - barAppearance.tintColor = UIColor(Color.jellyfinPurple) - } - - @ViewBuilder - var innerBody: some View { - if viewModel.isLoading { - ProgressView() - } else { - ScrollView { - VStack(alignment: .leading) { - 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 - let library = viewModel.libraries.first(where: { $0.id == libraryID }) - HStack { - Text("Latest \(library?.name ?? "")") - .font(.title2) - .fontWeight(.bold) - Spacer() - Button { - homeRouter - .route(to: \.library, (viewModel: .init(parentID: libraryID, - filters: viewModel.recentFilterSet), - title: library?.name ?? "")) - } label: { - HStack { - Text("See All").font(.subheadline).fontWeight(.bold) - Image(systemName: "chevron.right").font(Font.subheadline.bold()) - } - } - }.padding(.leading, 16) - .padding(.trailing, 16) - LatestMediaView(viewModel: .init(libraryID: libraryID)) - } - } - } - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) - } - } - } - - var body: some View { - innerBody - .navigationTitle(NSLocalizedString("Home", comment: "")) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - homeRouter.route(to: \.settings) - } label: { - Image(systemName: "gear") - } - } - } - } -} diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index b8c7ecaf..8ad4f31b 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -62,9 +62,14 @@ network. UIApplicationSupportsIndirectInputEvents UILaunchScreen - - UILaunchStoryboardName - VideoPlayer + + UIImageRespectsSafeAreaInsets + + UIImageName + swiftfin-logo + UIColorName + LaunchScreenBackground + UIRequiredDeviceCapabilities armv7 diff --git a/JellyfinPlayer/JellyfinPlayer.entitlements b/JellyfinPlayer/JellyfinPlayer.entitlements index b6b038ce..ee95ab7e 100644 --- a/JellyfinPlayer/JellyfinPlayer.entitlements +++ b/JellyfinPlayer/JellyfinPlayer.entitlements @@ -2,19 +2,9 @@ - com.apple.developer.coremedia.hls.low-latency - com.apple.security.app-sandbox - com.apple.security.application-groups - - group.me.vigue.jellyfin.mobileclient - com.apple.security.network.client - keychain-access-groups - - $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain - diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift deleted file mode 100644 index 74e2cedd..00000000 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ /dev/null @@ -1,254 +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 Defaults -import MessageUI -import Stinsen -import SwiftUI - -// The notification we'll send when a shake gesture happens. -extension UIDevice { - static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification") -} - -// Override the default behavior of shake gestures to send our notification instead. -extension UIWindow { - override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - if motion == .motionShake { - NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) - } - } -} - -// A view modifier that detects shaking and calls a function of our choosing. -struct DeviceShakeViewModifier: ViewModifier { - let action: () -> Void - - func body(content: Self.Content) -> some View { - content - .onAppear() - .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in - action() - } - } -} - -// A View extension to make the modifier easier to use. -extension View { - func onShake(perform action: @escaping () -> Void) -> some View { - modifier(DeviceShakeViewModifier(action: action)) - } -} - -extension UIDevice { - var hasNotch: Bool { - let bottom = UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.safeAreaInsets.bottom ?? 0 - return bottom > 0 - } -} - -extension View { - func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { - background(HostingWindowFinder(callback: callback)) - } -} - -struct HostingWindowFinder: UIViewRepresentable { - var callback: (UIWindow?) -> Void - - func makeUIView(context: Context) -> UIView { - let view = UIView() - DispatchQueue.main.async { [weak view] in - callback(view?.window) - } - return view - } - - func updateUIView(_ uiView: UIView, context: Context) {} -} - -struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey { - typealias Value = Bool - - static var defaultValue: Value = false - - static func reduce(value: inout Value, nextValue: () -> Value) { - value = nextValue() || value - } -} - -struct ViewPreferenceKey: PreferenceKey { - typealias Value = UIUserInterfaceStyle - - static var defaultValue: UIUserInterfaceStyle = .unspecified - - static func reduce(value: inout UIUserInterfaceStyle, nextValue: () -> UIUserInterfaceStyle) { - value = nextValue() - } -} - -struct SupportedOrientationsPreferenceKey: PreferenceKey { - typealias Value = UIInterfaceOrientationMask - static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown - - static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { - // use the most restrictive set from the stack - value.formIntersection(nextValue()) - } -} - -class PreferenceUIHostingController: UIHostingController { - init(wrappedView: V) { - let box = Box() - super.init(rootView: AnyView(wrappedView - .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { - box.value?._prefersHomeIndicatorAutoHidden = $0 - }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { - box.value?._orientations = $0 - }.onPreferenceChange(ViewPreferenceKey.self) { - box.value?._viewPreference = $0 - })) - box.value = self - } - - @objc dynamic required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - super.modalPresentationStyle = .fullScreen - } - - private class Box { - weak var value: PreferenceUIHostingController? - init() {} - } - - // MARK: Prefers Home Indicator Auto Hidden - - public var _prefersHomeIndicatorAutoHidden = false { - didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } - } - - override var prefersHomeIndicatorAutoHidden: Bool { - _prefersHomeIndicatorAutoHidden - } - - // MARK: Lock orientation - - public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown { - didSet { - if _orientations == .landscape { - let value = UIInterfaceOrientation.landscapeRight.rawValue - UIDevice.current.setValue(value, forKey: "orientation") - UIViewController.attemptRotationToDeviceOrientation() - } - } - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - _orientations - } - - public var _viewPreference: UIUserInterfaceStyle = .unspecified { - didSet { - overrideUserInterfaceStyle = _viewPreference - } - } -} - -extension View { - // Controls the application's preferred home indicator auto-hiding when this view is shown. - func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View { - preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value) - } - - func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View { - // When rendered, export the requested orientations upward to Root - preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations) - } - - func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View { - // When rendered, export the requested orientations upward to Root - preference(key: ViewPreferenceKey.self, value: viewPreference) - } -} - -class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { - public static let shared = EmailHelper() - override private init() { - // - } - - func sendLogs(logURL: URL) { - if !MFMailComposeViewController.canSendMail() { - // Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account") - return // EXIT - } - - let picker = MFMailComposeViewController() - - let fileManager = FileManager() - let data = fileManager.contents(atPath: logURL.path) - - picker.setSubject("[DEV-BUG] SwiftFin") - picker - .setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n", - isHTML: false) - picker.setToRecipients(["SwiftFin Bug Reports "]) - picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent) - picker.mailComposeDelegate = self - - EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil) - } - - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil) - } - - static func getRootViewController() -> UIViewController? { - UIApplication.shared.windows.first?.rootViewController - } -} - -@main -struct JellyfinPlayerApp: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @Default(.appAppearance) var appAppearance - - let persistenceController = PersistenceController.shared - - var body: some Scene { - WindowGroup { - EmptyView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .onAppear(perform: { - setupAppearance() - }) - .withHostingWindow { window in - window? - .rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view() - .environment(\.managedObjectContext, persistenceController.container.viewContext)) - } - .onShake { - EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) - } - .onOpenURL { url in - AppURLHandler.shared.processDeepLink(url: url) - } - } - } - - private func setupAppearance() { - UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style - } -} - -class AppDelegate: NSObject, UIApplicationDelegate { - static var orientationLock = UIInterfaceOrientationMask.all - - func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { - AppDelegate.orientationLock - } -} diff --git a/JellyfinPlayer/Objects/RefreshHelper.swift b/JellyfinPlayer/Objects/RefreshHelper.swift new file mode 100644 index 00000000..df2b7c3c --- /dev/null +++ b/JellyfinPlayer/Objects/RefreshHelper.swift @@ -0,0 +1,23 @@ +// + /* + * 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 UIKit + +// A more general derivative of +// https://stackoverflow.com/questions/65812080/introspect-library-uirefreshcontrol-with-swiftui-not-working +class RefreshHelper { + var refreshControl: UIRefreshControl? + var refreshAction: (() -> Void)? + + @objc func didRefresh() { + guard let refreshControl = refreshControl else { return } + refreshAction?() + refreshControl.endRefreshing() + } +} diff --git a/JellyfinPlayer/PersistenceController.swift b/JellyfinPlayer/PersistenceController.swift deleted file mode 100644 index 5e7b23ae..00000000 --- a/JellyfinPlayer/PersistenceController.swift +++ /dev/null @@ -1,55 +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 CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Model") - container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))] - - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (_, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift deleted file mode 100644 index 1235572e..00000000 --- a/JellyfinPlayer/SplashView.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -/* - * 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 Stinsen -import SwiftUI - -struct SplashView: View { - @EnvironmentObject var mainRouter: MainCoordinator.Router - @StateObject var viewModel = SplashViewModel() - - var body: some View { - ProgressView() - .onReceive(viewModel.$isLoggedIn) { flag in - if flag { - mainRouter.root(\.mainTab) - } else { - mainRouter.root(\.connectToServer) - } - } - } -} diff --git a/JellyfinPlayer/Views/BasicAppSettingsView.swift b/JellyfinPlayer/Views/BasicAppSettingsView.swift new file mode 100644 index 00000000..ed73b75f --- /dev/null +++ b/JellyfinPlayer/Views/BasicAppSettingsView.swift @@ -0,0 +1,72 @@ +// + /* + * 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 Defaults +import Stinsen +import SwiftUI + +struct BasicAppSettingsView: View { + + @EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router + @ObservedObject var viewModel: BasicAppSettingsViewModel + @State var resetTapped: Bool = false + + @Default(.appAppearance) var appAppearance + @Default(.defaultHTTPScheme) var defaultHTTPScheme + + var body: some View { + Form { + Section { + Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { + ForEach(self.viewModel.appearances, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) + } + }.onChange(of: appAppearance, perform: { _ in + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style + }) + } header: { + Text("Accessibility") + } + + Section { + Picker("Default Scheme", selection: $defaultHTTPScheme) { + ForEach(HTTPScheme.allCases, id: \.self) { scheme in + Text("\(scheme.rawValue)") + } + } + } header: { + Text("Networking") + } + + Button { + resetTapped = true + } label: { + Text("Reset") + } + } + .alert("Reset", isPresented: $resetTapped, actions: { + Button(role: .destructive) { + viewModel.reset() + basicAppSettingsRouter.dismissCoordinator() + } label: { + Text("Reset") + } + }) + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + basicAppSettingsRouter.dismissCoordinator() + } label: { + Image(systemName: "xmark.circle.fill") + } + } + } + } +} diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift new file mode 100644 index 00000000..f44975d6 --- /dev/null +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -0,0 +1,116 @@ +/* + * 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 Defaults +import Stinsen +import SwiftUI + +struct ConnectToServerView: View { + + @StateObject var viewModel: ConnectToServerViewModel + @State var uri = "" + + @Default(.defaultHTTPScheme) var defaultHTTPScheme + + var body: some View { + List { + Section { + TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) + .disableAutocorrection(true) + .autocapitalization(.none) + .keyboardType(.URL) + .onAppear { + if uri == "" { + uri = "\(defaultHTTPScheme.rawValue)://" + } + } + + if viewModel.isLoading { + Button(role: .destructive) { + viewModel.cancelConnection() + } label: { + Text("Cancel") + } + } else { + Button { + viewModel.connectToServer(uri: uri) + } label: { + Text("Connect") + } + .disabled(uri.isEmpty) + } + } header: { + Text("Connect to a Jellyfin server") + } + + Section { + if viewModel.searching { + HStack(alignment: .center, spacing: 5) { + Spacer() + // Oct. 15, 2021 + // There is a bug where ProgressView() won't appear sometimes when searching, + // dots were used instead but ProgressView() is preferred + Text("Searching...") + .foregroundColor(.secondary) + Spacer() + } + } else { + if viewModel.discoveredServers.isEmpty { + HStack(alignment: .center) { + Spacer() + Text("No local servers found") + .font(.callout) + .foregroundColor(.secondary) + Spacer() + } + } else { + ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in + Button { + uri = discoveredServer.url.absoluteString + viewModel.connectToServer(uri: discoveredServer.url.absoluteString) + } label: { + VStack(alignment: .leading, spacing: 5) { + Text(discoveredServer.name) + .font(.title3) + Text(discoveredServer.host) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .disabled(viewModel.isLoading) + } + } + } + } header: { + HStack { + Text("Local Servers") + Spacer() + + Button { + viewModel.discoverServers() + } label: { + Image(systemName: "arrow.clockwise.circle.fill") + } + .disabled(viewModel.searching || viewModel.isLoading) + } + } + .headerProminence(.increased) + } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), + dismissButton: .cancel()) + } + .navigationTitle("Connect") + .onAppear { + viewModel.discoverServers() + AppURLHandler.shared.appURLState = .allowedInLogin + } + .navigationBarBackButtonHidden(viewModel.isLoading) + } +} diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/Views/ContinueWatchingView.swift similarity index 100% rename from JellyfinPlayer/ContinueWatchingView.swift rename to JellyfinPlayer/Views/ContinueWatchingView.swift diff --git a/JellyfinPlayer/Views/HomeView.swift b/JellyfinPlayer/Views/HomeView.swift new file mode 100644 index 00000000..51b3ae1e --- /dev/null +++ b/JellyfinPlayer/Views/HomeView.swift @@ -0,0 +1,84 @@ +// +/* + * 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 Introspect +import SwiftUI + +struct HomeView: View { + + @EnvironmentObject var homeRouter: HomeCoordinator.Router + @StateObject var viewModel = HomeViewModel() + + private let refreshHelper = RefreshHelper() + + @ViewBuilder + var innerBody: some View { + if viewModel.isLoading { + ProgressView() + } else { + ScrollView { + VStack(alignment: .leading) { + if !viewModel.resumeItems.isEmpty { + ContinueWatchingView(items: viewModel.resumeItems) + } + if !viewModel.nextUpItems.isEmpty { + NextUpView(items: viewModel.nextUpItems) + } + + ForEach(viewModel.libraries, id: \.self) { library in + HStack { + Text("Latest \(library.name ?? "")") + .font(.title2) + .fontWeight(.bold) + Spacer() + Button { + homeRouter + .route(to: \.library, (viewModel: .init(parentID: library.id!, + filters: viewModel.recentFilterSet), + title: library.name ?? "")) + } label: { + HStack { + Text("See All").font(.subheadline).fontWeight(.bold) + Image(systemName: "chevron.right").font(Font.subheadline.bold()) + } + } + }.padding(.leading, 16) + .padding(.trailing, 16) + LatestMediaView(viewModel: .init(libraryID: library.id!)) + } + } + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) + } + .introspectScrollView { scrollView in + let control = UIRefreshControl() + + refreshHelper.refreshControl = control + refreshHelper.refreshAction = viewModel.refresh + + control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged) + scrollView.refreshControl = control + } + } + } + + var body: some View { + innerBody + .navigationTitle(NSLocalizedString("Home", comment: "")) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + homeRouter.route(to: \.settings) + } label: { + Image(systemName: "gearshape.fill") + } + } + } + } +} diff --git a/JellyfinPlayer/ItemView/ItemView.swift b/JellyfinPlayer/Views/ItemView/ItemView.swift similarity index 95% rename from JellyfinPlayer/ItemView/ItemView.swift rename to JellyfinPlayer/Views/ItemView/ItemView.swift index 445d4098..de799478 100644 --- a/JellyfinPlayer/ItemView/ItemView.swift +++ b/JellyfinPlayer/Views/ItemView/ItemView.swift @@ -28,7 +28,7 @@ struct ItemNavigationView: View { } } -private struct ItemView: View { +fileprivate struct ItemView: View { @EnvironmentObject var itemRouter: ItemCoordinator.Router @State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view. @@ -66,7 +66,7 @@ private struct ItemView: View { Label("Show Series", systemImage: "text.below.photo") } } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "ellipsis.circle.fill") } case .episode: Menu { @@ -81,7 +81,7 @@ private struct ItemView: View { Label("Show Season", systemImage: "square.fill.text.grid.1x2") } } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "ellipsis.circle.fill") } default: EmptyView() diff --git a/JellyfinPlayer/ItemView/ItemViewBody.swift b/JellyfinPlayer/Views/ItemView/ItemViewBody.swift similarity index 100% rename from JellyfinPlayer/ItemView/ItemViewBody.swift rename to JellyfinPlayer/Views/ItemView/ItemViewBody.swift diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift similarity index 100% rename from JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift rename to JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeTopBarView.swift b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift similarity index 100% rename from JellyfinPlayer/ItemView/Landscape/ItemLandscapeTopBarView.swift rename to JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift similarity index 100% rename from JellyfinPlayer/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift rename to JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift b/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitMainView.swift similarity index 100% rename from JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift rename to JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitMainView.swift diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/Views/LatestMediaView.swift similarity index 100% rename from JellyfinPlayer/LatestMediaView.swift rename to JellyfinPlayer/Views/LatestMediaView.swift diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/Views/LibraryFilterView.swift similarity index 98% rename from JellyfinPlayer/LibraryFilterView.swift rename to JellyfinPlayer/Views/LibraryFilterView.swift index 0a96a459..1b1ffb00 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/Views/LibraryFilterView.swift @@ -10,8 +10,8 @@ import Stinsen import SwiftUI struct LibraryFilterView: View { + @EnvironmentObject var filterRouter: FilterCoordinator.Router - @Environment(\.presentationMode) var presentationMode @Binding var filters: LibraryFilters var parentId: String = "" diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/Views/LibraryListView.swift similarity index 100% rename from JellyfinPlayer/LibraryListView.swift rename to JellyfinPlayer/Views/LibraryListView.swift diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/Views/LibrarySearchView.swift similarity index 100% rename from JellyfinPlayer/LibrarySearchView.swift rename to JellyfinPlayer/Views/LibrarySearchView.swift diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/Views/LibraryView.swift similarity index 100% rename from JellyfinPlayer/LibraryView.swift rename to JellyfinPlayer/Views/LibraryView.swift diff --git a/JellyfinPlayer/LoadingView.swift b/JellyfinPlayer/Views/LoadingView.swift similarity index 100% rename from JellyfinPlayer/LoadingView.swift rename to JellyfinPlayer/Views/LoadingView.swift diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/Views/NextUpView.swift similarity index 100% rename from JellyfinPlayer/NextUpView.swift rename to JellyfinPlayer/Views/NextUpView.swift diff --git a/JellyfinPlayer/ServerDetailView.swift b/JellyfinPlayer/Views/ServerDetailView.swift similarity index 81% rename from JellyfinPlayer/ServerDetailView.swift rename to JellyfinPlayer/Views/ServerDetailView.swift index 8c4b9b56..89fb08ab 100644 --- a/JellyfinPlayer/ServerDetailView.swift +++ b/JellyfinPlayer/Views/ServerDetailView.swift @@ -15,32 +15,32 @@ struct ServerDetailView: View { var body: some View { Form { - Section(header: Text("")) { + Section(header: Text("Server Details")) { HStack { Text("Name") Spacer() - Text(ServerEnvironment.current.server.name ?? "") + Text(SessionManager.main.currentLogin.server.name) .foregroundColor(.secondary) } HStack { Text("URI") Spacer() - Text(ServerEnvironment.current.server.baseURI ?? "") + Text(SessionManager.main.currentLogin.server.uri) .foregroundColor(.secondary) } HStack { Text("Version") Spacer() - Text(ServerEnvironment.current.server.version ?? "") + Text(SessionManager.main.currentLogin.server.version) .foregroundColor(.secondary) } HStack { Text("Operating System") Spacer() - Text(ServerEnvironment.current.server.os ?? "") + Text(SessionManager.main.currentLogin.server.os) .foregroundColor(.secondary) } } diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift new file mode 100644 index 00000000..bd0ea63e --- /dev/null +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -0,0 +1,140 @@ +// + /* + * 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 CoreStore +import SwiftUI + +struct ServerListView: View { + + @EnvironmentObject var serverListRouter: ServerListCoordinator.Router + @ObservedObject var viewModel: ServerListViewModel + + private var listView: some View { + ScrollView { + LazyVStack { + ForEach(viewModel.servers, id: \.id) { server in + Button { + serverListRouter.route(to: \.userList, server) + } label: { + ZStack(alignment: Alignment.leading) { + Rectangle() + .foregroundColor(Color(UIColor.secondarySystemFill)) + .frame(height: 100) + .cornerRadius(10) + + HStack(spacing: 10) { + Image(systemName: "server.rack") + .font(.system(size: 36)) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 5) { + Text(server.name) + .font(.title2) + .foregroundColor(.primary) + + Text(server.uri) + .font(.footnote) + .disabled(true) + .foregroundColor(.secondary) + + Text(viewModel.userTextFor(server: server)) + .font(.footnote) + .foregroundColor(.primary) + } + }.padding([.leading]) + } + .padding() + } + .contextMenu { + Button(role: .destructive) { + viewModel.remove(server: server) + } label: { + Label("Remove", systemImage: "trash") + } + } + } + } + } + } + + private var noServerView: some View { + VStack { + Text("Connect to a Jellyfin server to get started") + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + ZStack { + Rectangle() + .foregroundColor(Color.jellyfinPurple) + .frame(maxWidth: 400, maxHeight: 50) + .frame(height: 50) + .cornerRadius(10) + .padding(.horizontal, 30) + .padding([.top, .bottom], 20) + + Text("Connect") + .foregroundColor(Color.white) + .bold() + } + } + } + } + + @ViewBuilder + private var innerBody: some View { + if viewModel.servers.isEmpty { + noServerView + .offset(y: -50) + } else { + listView + } + } + + @ViewBuilder + private var trailingToolbarContent: some View { + if viewModel.servers.isEmpty { + EmptyView() + } else { + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + Image(systemName: "plus.circle.fill") + } + } + } + + private var leadingToolbarContent: some View { + Button { + serverListRouter.route(to: \.basicAppSettings) + } label: { + Image(systemName: "gearshape.fill") + } + } + + var body: some View { + innerBody + .navigationTitle("Servers") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + trailingToolbarContent + } + } + .toolbar(content: { + ToolbarItemGroup(placement: .navigationBarLeading) { + leadingToolbarContent + } + }) + .onAppear { + viewModel.fetchServers() + } + } +} diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift similarity index 74% rename from JellyfinPlayer/SettingsView.swift rename to JellyfinPlayer/Views/SettingsView.swift index c309b8c2..7b7a9ff5 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -11,9 +11,8 @@ import Stinsen import SwiftUI struct SettingsView: View { + @EnvironmentObject var settingsRouter: SettingsCoordinator.Router - @Environment(\.managedObjectContext) private var viewContext - @ObservedObject var viewModel: SettingsViewModel @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @@ -28,38 +27,61 @@ struct SettingsView: View { var body: some View { Form { Section(header: EmptyView()) { - HStack { - Text("User") - Spacer() - Text(SessionManager.current.user?.username ?? "") - .foregroundColor(.jellyfinPurple) - } - - Button { - settingsRouter.route(to: \.serverDetail) - } label: { + + // There is a bug where the SettingsView attmempts to remake itself upon signing out + // so this check is made + if SessionManager.main.currentLogin == nil { HStack { - Text("Server") + Text("User") Spacer() - Text(ServerEnvironment.current.server?.name ?? "") + Text("") .foregroundColor(.jellyfinPurple) + } - Image(systemName: "chevron.right") + Button { + settingsRouter.route(to: \.serverDetail) + } label: { + HStack { + Text("Server") + Spacer() + Text("") + .foregroundColor(.jellyfinPurple) + + Image(systemName: "chevron.right") + } + } + } else { + HStack { + Text("User") + Spacer() + Text(SessionManager.main.currentLogin.user.username) + .foregroundColor(.jellyfinPurple) + } + + Button { + settingsRouter.route(to: \.serverDetail) + } label: { + HStack { + Text("Server") + Spacer() + Text(SessionManager.main.currentLogin.server.name) + .foregroundColor(.jellyfinPurple) + + Image(systemName: "chevron.right") + } } } Button { - settingsRouter.dismissCoordinator() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) + settingsRouter.dismissCoordinator { + SessionManager.main.logout() } } label: { Text("Sign out") .font(.callout) } } + Section(header: Text("Playback")) { Picker("Default local quality", selection: $inNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in @@ -122,7 +144,7 @@ struct SettingsView: View { Button { settingsRouter.dismissCoordinator() } label: { - Image(systemName: "xmark") + Image(systemName: "xmark.circle.fill") } } } diff --git a/JellyfinPlayer/Views/UserListView.swift b/JellyfinPlayer/Views/UserListView.swift new file mode 100644 index 00000000..cd2f6411 --- /dev/null +++ b/JellyfinPlayer/Views/UserListView.swift @@ -0,0 +1,118 @@ +// + /* + * 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 UserListView: View { + + @EnvironmentObject var userListRouter: UserListCoordinator.Router + @ObservedObject var viewModel: UserListViewModel + + private var listView: some View { + ScrollView { + LazyVStack { + ForEach(viewModel.users, id: \.id) { user in + Button { + viewModel.login(user: user) + } label: { + ZStack(alignment: Alignment.leading) { + Rectangle() + .foregroundColor(Color(UIColor.secondarySystemFill)) + .frame(height: 50) + .cornerRadius(10) + + HStack { + Text(user.username) + .font(.title2) + + Spacer() + + if viewModel.isLoading { + ProgressView() + } + }.padding(.leading) + } + .padding() + } + .contextMenu { + Button(role: .destructive) { + viewModel.remove(user: user) + } label: { + Label("Remove", systemImage: "trash") + } + } + } + } + } + } + + private var noUserView: some View { + VStack { + Text("Sign in to get started") + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + Button { + userListRouter.route(to: \.userSignIn, viewModel.server) + } label: { + ZStack { + Rectangle() + .foregroundColor(Color.jellyfinPurple) + .frame(maxWidth: 400, maxHeight: 50) + .frame(height: 50) + .cornerRadius(10) + .padding(.horizontal, 30) + .padding([.top, .bottom], 20) + + Text("Sign in") + .foregroundColor(Color.white) + .bold() + } + } + } + } + + @ViewBuilder + private var innerBody: some View { + if viewModel.users.isEmpty { + noUserView + .offset(y: -50) + } else { + listView + } + } + + @ViewBuilder + private var toolbarContent: some View { + if viewModel.users.isEmpty { + EmptyView() + } else { + HStack { + Button { + userListRouter.route(to: \.userSignIn, viewModel.server) + } label: { + Image(systemName: "person.crop.circle.fill.badge.plus") + } + } + } + } + + var body: some View { + innerBody + .navigationTitle(viewModel.server.name) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + toolbarContent + } + } + .onAppear { + viewModel.fetchUsers() + } + } +} diff --git a/JellyfinPlayer/Views/UserSignInView.swift b/JellyfinPlayer/Views/UserSignInView.swift new file mode 100644 index 00000000..3173cdd7 --- /dev/null +++ b/JellyfinPlayer/Views/UserSignInView.swift @@ -0,0 +1,57 @@ +// + /* + * 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 Stinsen + +struct UserSignInView: View { + + @ObservedObject var viewModel: UserSignInViewModel + @State private var username: String = "" + @State private var password: String = "" + + var body: some View { + Form { + + Section { + TextField("Username", text: $username) + .disableAutocorrection(true) + .autocapitalization(.none) + + SecureField("Password", text: $password) + .disableAutocorrection(true) + .autocapitalization(.none) + + if viewModel.isLoading { + Button(role: .destructive) { + viewModel.cancelSignIn() + } label: { + Text("Cancel") + } + } else { + Button { + viewModel.login(username: username, password: password) + } label: { + Text("Sign In") + } + .disabled(username.isEmpty) + } + } header: { + Text("Sign In to \(viewModel.server.name)") + } + } + .alert(item: $viewModel.errorMessage) { _ in + Alert(title: Text(viewModel.alertTitle), + message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"), + dismissButton: .cancel()) + } + .navigationTitle("Sign In") + .navigationBarBackButtonHidden(viewModel.isLoading) + } +} diff --git a/JellyfinPlayer/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.storyboard similarity index 100% rename from JellyfinPlayer/VideoPlayer.storyboard rename to JellyfinPlayer/Views/VideoPlayer/VideoPlayer.storyboard diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift similarity index 97% rename from JellyfinPlayer/VideoPlayer.swift rename to JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift index cf29da12..e0ddcb71 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift @@ -518,13 +518,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), + let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) DispatchQueue.global(qos: .userInitiated).async { [self] in delegate?.showLoadingView(self) - MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.main.currentLogin.user.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) @@ -537,8 +537,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe switch err { case .error(401, _, _, _): self.delegate?.exitPlayer(self) - SessionManager.current.logout() - main?.root(\.connectToServer) + SessionManager.main.logout() case .error: self.delegate?.exitPlayer(self) } @@ -550,7 +549,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let mediaSource = response.mediaSources!.first.self! if mediaSource.transcodingUrl != nil { // Item is being transcoded by request of server - let streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)") + let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(mediaSource.transcodingUrl!)") let item = PlaybackItem() item.videoType = .transcode item.videoUrl = streamURL! @@ -564,7 +563,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl ?? "")")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl ?? "")")! } else { deliveryUrl = nil } @@ -596,9 +595,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.sendPlayReport() playbackItem = item } else { + // TODO: todo // Item will be directly played by the client. - let streamURL = - URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! + let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")! +// URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! let item = PlaybackItem() item.videoUrl = streamURL @@ -613,7 +613,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if stream.type == .subtitle { var deliveryUrl: URL? if stream.deliveryMethod == .external { - deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! + deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")! } else { deliveryUrl = nil } @@ -771,7 +771,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func getNextEpisode() { - TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, + TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.main.currentLogin.user.id, startItemId: manifest.id, limit: 2) .sink(receiveCompletion: { completion in print(completion) @@ -873,11 +873,11 @@ extension PlayerViewController: GCKGenericChannelDelegate { let payload: [String: Any] = [ "options": options, "command": command, - "userId": SessionManager.current.user.user_id!, - "deviceId": SessionManager.current.deviceID, - "accessToken": SessionManager.current.accessToken, - "serverAddress": ServerEnvironment.current.server.baseURI!, - "serverId": ServerEnvironment.current.server.server_id!, + "userId": SessionManager.main.currentLogin.user.id, +// "deviceId": SessionManager.main.currentLogin.de.deviceID, + "accessToken": SessionManager.main.currentLogin.user.accessToken, + "serverAddress": SessionManager.main.currentLogin.server.uri, + "serverId": SessionManager.main.currentLogin.server.id, "serverVersion": "10.8.0", "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, "subtitleBurnIn": false, @@ -931,7 +931,7 @@ extension PlayerViewController: GCKSessionManagerListener { let playNowOptions: [String: Any] = [ "items": [[ "Id": manifest.id!, - "ServerId": ServerEnvironment.current.server.server_id!, + "ServerId": SessionManager.main.currentLogin.server.id, "Name": manifest.name!, "Type": manifest.type!, "MediaType": manifest.mediaType!, diff --git a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayerCastDeviceSelector.swift similarity index 100% rename from JellyfinPlayer/VideoPlayerCastDeviceSelector.swift rename to JellyfinPlayer/Views/VideoPlayer/VideoPlayerCastDeviceSelector.swift diff --git a/JellyfinPlayer/VideoPlayerSettingsView.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayerSettingsView.swift similarity index 100% rename from JellyfinPlayer/VideoPlayerSettingsView.swift rename to JellyfinPlayer/Views/VideoPlayer/VideoPlayerSettingsView.swift diff --git a/JellyfinPlayer/VideoUpNextView.swift b/JellyfinPlayer/Views/VideoPlayer/VideoUpNextView.swift similarity index 100% rename from JellyfinPlayer/VideoUpNextView.swift rename to JellyfinPlayer/Views/VideoPlayer/VideoUpNextView.swift diff --git a/Shared/Coordinators/BasicAppSettingsCoordinator.swift b/Shared/Coordinators/BasicAppSettingsCoordinator.swift new file mode 100644 index 00000000..d9ee38bb --- /dev/null +++ b/Shared/Coordinators/BasicAppSettingsCoordinator.swift @@ -0,0 +1,23 @@ +// + /* + * 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 Stinsen +import SwiftUI + +final class BasicAppSettingsCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start) + + @Root var start = makeStart + + @ViewBuilder func makeStart() -> some View { + BasicAppSettingsView(viewModel: BasicAppSettingsViewModel()) + } +} diff --git a/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift b/Shared/Coordinators/ConnectToServerCoodinator.swift similarity index 64% rename from JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift rename to Shared/Coordinators/ConnectToServerCoodinator.swift index 5f81bd85..45d47f03 100644 --- a/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift +++ b/Shared/Coordinators/ConnectToServerCoodinator.swift @@ -12,11 +12,17 @@ import Stinsen import SwiftUI final class ConnectToServerCoodinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \ConnectToServerCoodinator.start) @Root var start = makeStart + @Route(.push) var userSignIn = makeUserSignIn + + func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { + return UserSignInCoordinator(viewModel: .init(server: server)) + } @ViewBuilder func makeStart() -> some View { - ConnectToServerView() + ConnectToServerView(viewModel: ConnectToServerViewModel()) } } diff --git a/JellyfinPlayer/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift similarity index 99% rename from JellyfinPlayer/Coordinators/FilterCoordinator.swift rename to Shared/Coordinators/FilterCoordinator.swift index 48496d14..fa845d0c 100644 --- a/JellyfinPlayer/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -14,7 +14,9 @@ import SwiftUI typealias FilterCoordinatorParams = (filters: Binding, enabledFilterType: [FilterType], parentId: String) final class FilterCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \FilterCoordinator.start) + @Root var start = makeStart @Binding var filters: LibraryFilters diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift similarity index 67% rename from JellyfinPlayer/Coordinators/HomeCoordinator.swift rename to Shared/Coordinators/HomeCoordinator.swift index be38b278..e30c79af 100644 --- a/JellyfinPlayer/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -13,12 +13,15 @@ import Stinsen import SwiftUI final class HomeCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \HomeCoordinator.start) @Root var start = makeStart @Route(.modal) var settings = makeSettings @Route(.push) var library = makeLibrary @Route(.push) var item = makeItem + @Route(.modal) var modalItem = makeModalItem + @Route(.modal) var modalLibrary = makeModalLibrary func makeSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(SettingsCoordinator()) @@ -31,6 +34,14 @@ final class HomeCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { + return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) + } @ViewBuilder func makeStart() -> some View { HomeView() diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift new file mode 100644 index 00000000..5e578efb --- /dev/null +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -0,0 +1,45 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class ItemCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \ItemCoordinator.start) + + @Root var start = makeStart + @Route(.push) var item = makeItem + @Route(.push) var library = makeLibrary + @Route(.fullScreen) var videoPlayer = makeVideoPlayer + + let itemDto: BaseItemDto + + init(item: BaseItemDto) { + self.itemDto = item + } + + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { + LibraryCoordinator(viewModel: params.viewModel, title: params.title) + } + + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } + + func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) + } + + @ViewBuilder func makeStart() -> some View { + ItemNavigationView(item: itemDto) + } +} diff --git a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift similarity index 84% rename from JellyfinPlayer/Coordinators/LibraryCoordinator.swift rename to Shared/Coordinators/LibraryCoordinator.swift index 47f45978..a5ef3495 100644 --- a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -15,15 +15,17 @@ import SwiftUI typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String) final class LibraryCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \LibraryCoordinator.start) @Root var start = makeStart @Route(.push) var search = makeSearch @Route(.modal) var filter = makeFilter @Route(.push) var item = makeItem + @Route(.modal) var modalItem = makeModalItem - var viewModel: LibraryViewModel - var title: String + let viewModel: LibraryViewModel + let title: String init(viewModel: LibraryViewModel, title: String) { self.viewModel = viewModel @@ -47,4 +49,8 @@ final class LibraryCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } } diff --git a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift similarity index 99% rename from JellyfinPlayer/Coordinators/LibraryListCoordinator.swift rename to Shared/Coordinators/LibraryListCoordinator.swift index 2ff63ad5..88377644 100644 --- a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -12,6 +12,7 @@ import Stinsen import SwiftUI final class LibraryListCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \LibraryListCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift new file mode 100644 index 00000000..9c82fd44 --- /dev/null +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -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 Foundation +import Nuke +import Stinsen +import SwiftUI +import WidgetKit + +final class MainCoordinator: NavigationCoordinatable { + var stack: NavigationStack + + @Root var mainTab = makeMainTab + @Root var serverList = makeServerList + + init() { + if SessionManager.main.currentLogin != nil { + self.stack = NavigationStack(initial: \MainCoordinator.mainTab) + } else { + self.stack = NavigationStack(initial: \MainCoordinator.serverList) + } + + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory + DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk + + WidgetCenter.shared.reloadAllTimelines() + UIScrollView.appearance().keyboardDismissMode = .onDrag + + // Back bar button item setup + let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") + let barAppearance = UINavigationBar.appearance() + barAppearance.backIndicatorImage = backButtonBackgroundImage + barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage + barAppearance.tintColor = UIColor(Color.jellyfinPurple) + + // Notification setup for state + let nc = SwiftfinNotificationCenter.main + nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) + nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) + nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil) + } + + @objc func didLogIn() { + LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.") + root(\.mainTab) + } + + @objc func didLogOut() { + LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.") + root(\.serverList) + } + + @objc func processDeepLink(_ notification: Notification) { + guard let deepLink = notification.object as? DeepLink else { return } + if let coordinator = hasRoot(\.mainTab) { + switch deepLink { + case let .item(item): + coordinator.focusFirst(\.home) + .child + .popToRoot() + .route(to: \.item, item) + } + } + } + func makeMainTab() -> MainTabCoordinator { + MainTabCoordinator() + } + + func makeServerList() -> NavigationViewCoordinator { + NavigationViewCoordinator(ServerListCoordinator()) + } +} diff --git a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift similarity index 81% rename from JellyfinPlayer/Coordinators/MainTabCoordinator.swift rename to Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index d5f430ab..3be7131b 100644 --- a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -9,17 +9,16 @@ import Foundation import SwiftUI - import Stinsen final class MainTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ \MainTabCoordinator.home, - \MainTabCoordinator.allMedia, + \MainTabCoordinator.allMedia ]) @Route(tabItem: makeHomeTab) var home = makeHome - @Route(tabItem: makeTodosTab) var allMedia = makeTodos + @Route(tabItem: makeAllMediaTab) var allMedia = makeAllMedia func makeHome() -> NavigationViewCoordinator { return NavigationViewCoordinator(HomeCoordinator()) @@ -30,11 +29,11 @@ final class MainTabCoordinator: TabCoordinatable { Text("Home") } - func makeTodos() -> NavigationViewCoordinator { + func makeAllMedia() -> NavigationViewCoordinator { return NavigationViewCoordinator(LibraryListCoordinator()) } - @ViewBuilder func makeTodosTab(isActive: Bool) -> some View { + @ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View { Image(systemName: "folder") Text("All Media") } @@ -42,6 +41,7 @@ final class MainTabCoordinator: TabCoordinatable { @ViewBuilder func customize(_ view: AnyView) -> some View { view.onAppear { AppURLHandler.shared.appURLState = .allowed + // TODO: todo DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { AppURLHandler.shared.processLaunchedURLIfNeeded() } diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift new file mode 100644 index 00000000..05d77b94 --- /dev/null +++ b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift @@ -0,0 +1,54 @@ +// + /* + * 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 Nuke +import Stinsen +import SwiftUI + +final class MainCoordinator: NavigationCoordinatable { + var stack = NavigationStack(initial: \MainCoordinator.mainTab) + + @Root var mainTab = makeMainTab + @Root var serverList = makeServerList + + init() { + if SessionManager.main.currentLogin != nil { + self.stack = NavigationStack(initial: \MainCoordinator.mainTab) + } else { + self.stack = NavigationStack(initial: \MainCoordinator.serverList) + } + + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory + DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk + + // Notification setup for state + let nc = SwiftfinNotificationCenter.main + nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) + nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) + } + + @objc func didLogIn() { + LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") + root(\.mainTab) + } + + @objc func didLogOut() { + LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") + root(\.serverList) + } + + func makeMainTab() -> MainTabCoordinator { + MainTabCoordinator() + } + + func makeServerList() -> NavigationViewCoordinator { + NavigationViewCoordinator(ServerListCoordinator()) + } +} diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift new file mode 100644 index 00000000..8be5a5c6 --- /dev/null +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -0,0 +1,83 @@ +// + /* + * 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 +import Stinsen + +final class MainTabCoordinator: TabCoordinatable { + var child = TabChild(startingItems: [ + \MainTabCoordinator.home, + \MainTabCoordinator.tv, + \MainTabCoordinator.movies, + \MainTabCoordinator.other, + \MainTabCoordinator.settings + ]) + + @Route(tabItem: makeHomeTab) var home = makeHome + @Route(tabItem: makeTvTab) var tv = makeTv + @Route(tabItem: makeMoviesTab) var movies = makeMovies + @Route(tabItem: makeOtherTab) var other = makeOther + @Route(tabItem: makeSettingsTab) var settings = makeSettings + + func makeHome() -> NavigationViewCoordinator { + return NavigationViewCoordinator(HomeCoordinator()) + } + + @ViewBuilder func makeHomeTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "house") + Text("Home") + } + } + + func makeTv() -> NavigationViewCoordinator { + return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows")) + } + + @ViewBuilder func makeTvTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "tv") + Text("TV Shows") + } + } + + func makeMovies() -> NavigationViewCoordinator { + return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies")) + } + + @ViewBuilder func makeMoviesTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "film") + Text("Movies") + } + } + + func makeOther() -> NavigationViewCoordinator { + return NavigationViewCoordinator(LibraryListCoordinator()) + } + + @ViewBuilder func makeOtherTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "folder") + Text("Other") + } + } + + func makeSettings() -> NavigationViewCoordinator { + return NavigationViewCoordinator(SettingsCoordinator()) + } + + @ViewBuilder func makeSettingsTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "gearshape.fill") + Text("Settings") + } + } +} diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift new file mode 100644 index 00000000..9c530d3c --- /dev/null +++ b/Shared/Coordinators/MoviesLibrariesCoordinator.swift @@ -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 JellyfinAPI +import Stinsen +import SwiftUI + +final class MovieLibrariesCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start) + + @Root var start = makeStart + @Route(.push) var library = makeLibrary + + let viewModel: MovieLibrariesViewModel + let title: String + + init(viewModel: MovieLibrariesViewModel, title: String) { + self.viewModel = viewModel + self.title = title + } + + @ViewBuilder func makeStart() -> some View { + MovieLibrariesView(viewModel: self.viewModel, title: title) + } + + func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { + LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + } +} diff --git a/JellyfinPlayer/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift similarity index 94% rename from JellyfinPlayer/Coordinators/SearchCoordinator.swift rename to Shared/Coordinators/SearchCoordinator.swift index 60c761d2..9d66e6b4 100644 --- a/JellyfinPlayer/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -13,12 +13,13 @@ import SwiftUI import JellyfinAPI final class SearchCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \SearchCoordinator.start) @Root var start = makeStart @Route(.push) var item = makeItem - var viewModel: LibrarySearchViewModel + let viewModel: LibrarySearchViewModel init(viewModel: LibrarySearchViewModel) { self.viewModel = viewModel diff --git a/Shared/Coordinators/ServerListCoordinator.swift b/Shared/Coordinators/ServerListCoordinator.swift new file mode 100644 index 00000000..d60abebd --- /dev/null +++ b/Shared/Coordinators/ServerListCoordinator.swift @@ -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 Stinsen +import SwiftUI + +final class ServerListCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \ServerListCoordinator.start) + + @Root var start = makeStart + @Route(.push) var connectToServer = makeConnectToServer + @Route(.push) var userList = makeUserList + @Route(.modal) var basicAppSettings = makeBasicAppSettings + + func makeConnectToServer() -> ConnectToServerCoodinator { + ConnectToServerCoodinator() + } + + func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator { + UserListCoordinator(viewModel: .init(server: server)) + } + + func makeBasicAppSettings() -> NavigationViewCoordinator { + NavigationViewCoordinator(BasicAppSettingsCoordinator()) + } + + @ViewBuilder func makeStart() -> some View { + ServerListView(viewModel: ServerListViewModel()) + } +} diff --git a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift similarity index 99% rename from JellyfinPlayer/Coordinators/SettingsCoordinator.swift rename to Shared/Coordinators/SettingsCoordinator.swift index cbf6b1e0..0b8f8a23 100644 --- a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -12,6 +12,7 @@ import Stinsen import SwiftUI final class SettingsCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \SettingsCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift new file mode 100644 index 00000000..2ad50744 --- /dev/null +++ b/Shared/Coordinators/TVLibrariesCoordinator.swift @@ -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 JellyfinAPI +import Stinsen +import SwiftUI + +final class TVLibrariesCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \TVLibrariesCoordinator.start) + + @Root var start = makeStart + @Route(.push) var library = makeLibrary + + let viewModel: TVLibrariesViewModel + let title: String + + init(viewModel: TVLibrariesViewModel, title: String) { + self.viewModel = viewModel + self.title = title + } + + @ViewBuilder func makeStart() -> some View { + TVLibrariesView(viewModel: self.viewModel, title: title) + } + + func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { + LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + } +} diff --git a/Shared/Coordinators/UserListCoordinator.swift b/Shared/Coordinators/UserListCoordinator.swift new file mode 100644 index 00000000..ff728bb6 --- /dev/null +++ b/Shared/Coordinators/UserListCoordinator.swift @@ -0,0 +1,34 @@ +// + /* + * 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 Stinsen +import SwiftUI + +final class UserListCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \UserListCoordinator.start) + + @Root var start = makeStart + @Route(.push) var userSignIn = makeUserSignIn + + let viewModel: UserListViewModel + + init(viewModel: UserListViewModel) { + self.viewModel = viewModel + } + + func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { + return UserSignInCoordinator(viewModel: .init(server: server)) + } + + @ViewBuilder func makeStart() -> some View { + UserListView(viewModel: viewModel) + } +} diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift new file mode 100644 index 00000000..f4e03a87 --- /dev/null +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -0,0 +1,29 @@ +// + /* + * 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 Stinsen +import SwiftUI + +final class UserSignInCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \UserSignInCoordinator.start) + + @Root var start = makeStart + + let viewModel: UserSignInViewModel + + init(viewModel: UserSignInViewModel) { + self.viewModel = viewModel + } + + @ViewBuilder func makeStart() -> some View { + UserSignInView(viewModel: viewModel) + } +} diff --git a/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator.swift similarity index 94% rename from JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift rename to Shared/Coordinators/VideoPlayerCoordinator.swift index ebe38123..921d52f9 100644 --- a/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator.swift @@ -13,10 +13,12 @@ import Stinsen import SwiftUI final class VideoPlayerCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \VideoPlayerCoordinator.start) @Root var start = makeStart - var item: BaseItemDto + + let item: BaseItemDto init(item: BaseItemDto) { self.item = item diff --git a/Shared/Errors/ErrorMessage.swift b/Shared/Errors/ErrorMessage.swift index fab486f7..0f14fe59 100644 --- a/Shared/Errors/ErrorMessage.swift +++ b/Shared/Errors/ErrorMessage.swift @@ -16,6 +16,10 @@ struct ErrorMessage: Identifiable { let title: String let displayMessage: String let logConstructor: LogConstructor + + // Chosen value such that if an error has this code, don't show the code to the UI + // This was chosen because of its unlikelyhood to ever be used + static let noShowErrorCode = -69420 var id: String { return "\(code)\(title)\(logConstructor.message)" diff --git a/Shared/Extensions/DefaultsExtension.swift b/Shared/Extensions/DefaultsExtension.swift deleted file mode 100644 index 30cb48c9..00000000 --- a/Shared/Extensions/DefaultsExtension.swift +++ /dev/null @@ -1,22 +0,0 @@ -// - /* - * 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 Defaults - -extension Defaults.Keys { - static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000) - static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000) - static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false) - static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto") - static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto") - static let appAppearance = Key("appAppearance", default: .system) - static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .thirty) - static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .thirty) -} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 83ea48eb..78ad8991 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -74,7 +74,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -91,7 +91,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -100,7 +100,7 @@ public extension BaseItemDto { let imageTag = seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } @@ -117,7 +117,7 @@ public extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = - "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" // print(urlString) return URL(string: urlString)! } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index a3a9c042..63b2d239 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -57,7 +57,7 @@ extension BaseItemPerson { // MARK: PortraitImageStackable extension BaseItemPerson: PortraitImageStackable { public func imageURLContsructor(maxWidth: Int) -> URL { - return self.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: maxWidth) + return self.getImage(baseURL: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth) } public var title: String { diff --git a/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift new file mode 100644 index 00000000..f74a5a3c --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift @@ -0,0 +1,23 @@ +// + /* + * 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 + +struct JellyfinAPIError: Error { + + private let message: String + + init(_ message: String) { + self.message = message + } + + var localizedDescription: String { + return message + } +} diff --git a/Shared/Extensions/UIDeviceExtensions.swift b/Shared/Extensions/UIDeviceExtensions.swift new file mode 100644 index 00000000..e837782f --- /dev/null +++ b/Shared/Extensions/UIDeviceExtensions.swift @@ -0,0 +1,16 @@ +// + /* + * 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 UIKit + +extension UIDevice { + static var vendorUUIDString: String { + return current.identifierForVendor!.uuidString + } +} diff --git a/Shared/Objects/AppAppearance.swift b/Shared/Objects/AppAppearance.swift new file mode 100644 index 00000000..a3bc58a5 --- /dev/null +++ b/Shared/Objects/AppAppearance.swift @@ -0,0 +1,32 @@ +// + /* + * 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 Defaults +import SwiftUI + +enum AppAppearance: String, CaseIterable, Defaults.Serializable { + case system + case dark + case light + + var localizedName: String { + return NSLocalizedString(self.rawValue.capitalized, comment: "") + } + + var style: UIUserInterfaceStyle { + switch self { + case .system: + return .unspecified + case .dark: + return .dark + case .light: + return .light + } + } +} diff --git a/Shared/Objects/Bitrates.swift b/Shared/Objects/Bitrates.swift new file mode 100644 index 00000000..6a07989f --- /dev/null +++ b/Shared/Objects/Bitrates.swift @@ -0,0 +1,15 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation + +struct Bitrates: Codable, Hashable { + public var name: String + public var value: Int +} diff --git a/Shared/Objects/HTTPScheme.swift b/Shared/Objects/HTTPScheme.swift new file mode 100644 index 00000000..28152234 --- /dev/null +++ b/Shared/Objects/HTTPScheme.swift @@ -0,0 +1,16 @@ +// + /* + * 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 Defaults +import Foundation + +enum HTTPScheme: String, Defaults.Serializable, CaseIterable { + case http + case https +} diff --git a/Shared/Objects/PillStackable.swift b/Shared/Objects/PillStackable.swift new file mode 100644 index 00000000..4d0296eb --- /dev/null +++ b/Shared/Objects/PillStackable.swift @@ -0,0 +1,14 @@ +// + /* + * 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 + +protocol PillStackable { + var title: String { get } +} diff --git a/Shared/Objects/PortraitImageStackable.swift b/Shared/Objects/PortraitImageStackable.swift new file mode 100644 index 00000000..e866de31 --- /dev/null +++ b/Shared/Objects/PortraitImageStackable.swift @@ -0,0 +1,18 @@ +// + /* + * 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 + +public protocol PortraitImageStackable { + func imageURLContsructor(maxWidth: Int) -> URL + var title: String { get } + var description: String? { get } + var blurHash: String { get } + var failureInitials: String { get } +} diff --git a/Shared/Objects/TrackLanguage.swift b/Shared/Objects/TrackLanguage.swift new file mode 100644 index 00000000..03833245 --- /dev/null +++ b/Shared/Objects/TrackLanguage.swift @@ -0,0 +1,17 @@ +// + /* + * 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 + +struct TrackLanguage: Hashable { + var name: String + var isoCode: String + + static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") +} diff --git a/Shared/Resources/Model.xcdatamodeld/.xccurrentversion b/Shared/Resources/Model.xcdatamodeld/.xccurrentversion index ea5bdb72..0c67376e 100644 --- a/Shared/Resources/Model.xcdatamodeld/.xccurrentversion +++ b/Shared/Resources/Model.xcdatamodeld/.xccurrentversion @@ -1,8 +1,5 @@ - - _XCCurrentVersionName - JellyfinPlayer.xcdatamodel - + diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift similarity index 81% rename from Shared/ServerLocator/ServerDiscovery.swift rename to Shared/ServerDiscovery/ServerDiscovery.swift index 911239cd..6f34789d 100644 --- a/Shared/ServerLocator/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -11,22 +11,6 @@ import Foundation public class ServerDiscovery { - public struct ServerCredential: Codable { - public let host: String - public let port: Int - public let username: String - public let password: String - public let deviceId: String - - public init(_ host: String, _ port: Int, _ username: String, _ password: String, _ deviceId: String = UUID().uuidString) { - self.host = host - self.port = port - self.username = username - self.password = password - self.deviceId = deviceId - } - } - public struct ServerLookupResponse: Codable, Hashable, Identifiable { public func hash(into hasher: inout Hasher) { @@ -53,7 +37,7 @@ public class ServerDiscovery { if let port = components?.port { return port } - return 8096 + return 7359 } enum CodingKeys: String, CodingKey { @@ -62,6 +46,7 @@ public class ServerDiscovery { case name = "Name" } } + private let broadcastConn: UDPBroadcastConnection public init() { diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerDiscovery/UDPBroadCastConnection.swift similarity index 100% rename from Shared/ServerLocator/UDPBroadCastConnection.swift rename to Shared/ServerDiscovery/UDPBroadCastConnection.swift diff --git a/Shared/Singleton/ServerEnvironment.swift b/Shared/Singleton/ServerEnvironment.swift deleted file mode 100644 index 326ede82..00000000 --- a/Shared/Singleton/ServerEnvironment.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -/* - * 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 CoreData -import Foundation -import JellyfinAPI - -final class ServerEnvironment { - static let current = ServerEnvironment() - fileprivate(set) var server: Server! - - init() { - let serverRequest: NSFetchRequest = Server.fetchRequest() - let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) - - if servers?.count != 0 { - server = servers?.first - JellyfinAPI.basePath = server.baseURI! - } - } - - func create(with uri: String) -> AnyPublisher { - LogManager.shared.log.debug("Initializing new Server object with raw URI: \"\(uri)\"") - var uri = uri - if !uri.contains("http") { - uri = "https://" + uri - } - if uri.last == "/" { - uri = String(uri.dropLast()) - } - LogManager.shared.log.debug("Normalized URI: \"\(uri)\", attempting to getPublicSystemInfo()") - - JellyfinAPI.basePath = uri - return SystemAPI.getPublicSystemInfo() - .map { response in - let server = Server(context: PersistenceController.shared.container.viewContext) - server.baseURI = uri - server.name = response.serverName - server.server_id = response.id - server.version = response.version - server.os = response.operatingSystem - return server - } - .handleEvents(receiveOutput: { [unowned self] response in - server = response - _ = try? PersistenceController.shared.container.viewContext.save() - }).eraseToAnyPublisher() - } - - func reset() { - JellyfinAPI.basePath = "" - server = nil - - let serverRequest: NSFetchRequest = Server.fetchRequest() - let deleteRequest = NSBatchDeleteRequest(fetchRequest: serverRequest) - - // coredata will theoretically never throw - _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) - } -} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 9a69bc28..d5fdd1c0 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -9,179 +9,245 @@ import Combine import CoreData +import CoreStore +import Defaults import Foundation import JellyfinAPI -import KeychainSwift import UIKit -#if os(tvOS) -import TVServices -#endif +typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) +// MARK: NewSessionManager final class SessionManager { - static let current = SessionManager() - fileprivate(set) var user: SignedInUser! - fileprivate(set) var deviceID: String = "" - fileprivate(set) var accessToken: String = "" - - #if os(tvOS) - let tvUserManager = TVUserManager() - #endif - let userDefaults = UserDefaults() - - init() { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - let lastUsedUserID = userDefaults.string(forKey: "lastUsedUserID") - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - - #if os(tvOS) - savedUsers?.forEach { savedUser in - if savedUser.appletv_id == tvUserManager.currentUserIdentifier ?? "" { - self.user = savedUser - } - } - #else - if lastUsedUserID != nil { - savedUsers?.forEach { savedUser in - if savedUser.user_id ?? "" == lastUsedUserID! { - user = savedUser - } - } - } else { - user = savedUsers?.first - } - #endif - - if user != nil { - let authToken = getAuthToken(userID: user.user_id!) - generateAuthHeader(with: authToken, deviceID: user.device_uuid) + + // MARK: currentLogin + private(set) var currentLogin: CurrentLogin! + + // MARK: main + static let main = SessionManager() + + private init() { + if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID], + let user = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", lastUserID)]) { + + guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") } + guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return } + + JellyfinAPI.basePath = server.uri + setAuthHeader(with: accessToken.value) + currentLogin = (server: existingServer.state, user: user.state) } } - - fileprivate func generateAuthHeader(with authToken: String?, deviceID devID: String?) { + + private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String { + return "\(server.id)-\(user.id)" + } + + func fetchServers() -> [SwiftfinStore.State.Server] { + let servers = try! SwiftfinStore.dataStack.fetchAll(From()) + return servers.map({ $0.state }) + } + + func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] { + guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From(), + Where("id == %@", server.id)) + else { fatalError("No stored server associated with given state server?") } + return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username }) + } + + // Connects to a server at the given uri, storing if successful + func connectToServer(with uri: String) -> AnyPublisher { + var uriComponents = URLComponents(string: uri) ?? URLComponents() + + if uriComponents.scheme == nil { + uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue + } + + var uri = uriComponents.string ?? "" + + if uri.last == "/" { + uri = String(uri.dropLast()) + } + + JellyfinAPI.basePath = uri + + return SystemAPI.getPublicSystemInfo() + .tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + let newServer = transaction.create(Into()) + + guard let name = response.serverName, + let id = response.id, + let os = response.operatingSystem, + let version = response.version else { throw JellyfinAPIError("Missing server data from network call") } + + newServer.uri = uri + newServer.name = name + newServer.id = id + newServer.os = os + newServer.version = version + newServer.users = [] + + // Check for existing server on device + if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", newServer.id)]) { + throw SwiftfinStore.Errors.existingServer(existingServer.state) + } + + return (newServer, transaction) + }) + .handleEvents(receiveOutput: { (_, transaction) in + try? transaction.commitAndWait() + }) + .map({ (server, _) in + return server.state + }) + .eraseToAnyPublisher() + } + + // Logs in a user with an associated server, storing if successful + func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher { + setAuthHeader(with: "") + + JellyfinAPI.basePath = server.uri + + return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) + .tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in + + guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") } + + let transaction = SwiftfinStore.dataStack.beginUnsafe() + let newUser = transaction.create(Into()) + + guard let username = response.user?.name, + let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") } + + newUser.username = username + newUser.id = id + newUser.appleTVID = "" + + // Check for existing user on device + if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", newUser.id)]) { + throw SwiftfinStore.Errors.existingUser(existingUser.state) + } + + let newAccessToken = transaction.create(Into()) + newAccessToken.value = accessToken + newUser.accessToken = newAccessToken + + guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", server.id)]) + else { fatalError("No stored server associated with given state server?") } + + guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") } + editUserServer.users.insert(newUser) + + return (editUserServer, newUser, transaction) + }) + .handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in + setAuthHeader(with: user.accessToken?.value ?? "") + try? transaction.commitAndWait() + + // Fetch for the right queue + let currentServer = SwiftfinStore.dataStack.fetchExisting(server)! + let currentUser = SwiftfinStore.dataStack.fetchExisting(user)! + + SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id + + currentLogin = (server: currentServer.state, user: currentUser.state) + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) + }) + .map({ (_, user, _) in + return user.state + }) + .eraseToAnyPublisher() + } + + func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { + JellyfinAPI.basePath = server.uri + SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id + setAuthHeader(with: user.accessToken) + currentLogin = (server: server, user: user) + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) + } + + func logout() { + currentLogin = nil + JellyfinAPI.basePath = "" + setAuthHeader(with: "") + SwiftfinStore.Defaults.suite[.lastServerUserID] = nil + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) + } + + func purge() { + // Delete all servers + let servers = fetchServers() + + for server in servers { + delete(server: server) + } + + // Delete UserDefaults + SwiftfinStore.Defaults.suite.removeAll() + + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) + } + + func delete(user: SwiftfinStore.State.User) { + guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", user.id)]) else { fatalError("No stored user for state user?")} + _delete(user: storedUser, transaction: nil) + } + + func delete(server: SwiftfinStore.State.Server) { + guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From(), + [Where("id == %@", server.id)]) else { fatalError("No stored server for state server?")} + _delete(server: storedServer, transaction: nil) + } + + private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) { + guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")} + + let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! + transaction.delete(storedAccessToken) + transaction.delete(user) + try? transaction.commitAndWait() + } + + private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) { + let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! + + for user in server.users { + _delete(user: user, transaction: transaction) + } + + transaction.delete(server) + try? transaction.commitAndWait() + } + + private func setAuthHeader(with accessToken: String) { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String var deviceName = UIDevice.current.name deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) - deviceName = String(deviceName.unicodeScalars.filter {CharacterSet.urlQueryAllowed.contains($0) }) - - var header = "MediaBrowser " + deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) }) + + let platform: String #if os(tvOS) - header.append("Client=\"Jellyfin tvOS\", ") + platform = "tvOS" #else - header.append("Client=\"SwiftFin iOS\", ") + platform = "iOS" #endif - + + var header = "MediaBrowser " + header.append("Client=\"Jellyfin \(platform)\", ") header.append("Device=\"\(deviceName)\", ") - - if devID == nil { - LogManager.shared.log.info("Generating device ID...") - #if os(tvOS) - header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") - deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" - #else - header.append("DeviceId=\"iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))\", ") - deviceID = "iOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(String(Date().timeIntervalSince1970))" - #endif - } else { - LogManager.shared.log.info("Using stored device ID...") - header.append("DeviceId=\"\(devID!)\", ") - deviceID = devID! - } - + header.append("DeviceId=\"\(platform)_\(UIDevice.vendorUUIDString)_\(String(Date().timeIntervalSince1970))\", ") header.append("Version=\"\(appVersion ?? "0.0.1")\", ") - - if authToken != nil { - header.append("Token=\"\(authToken!)\"") - accessToken = authToken! - } + header.append("Token=\"\(accessToken)\"") JellyfinAPI.customHeaders["X-Emby-Authorization"] = header } - - fileprivate func getAuthToken(userID: String) -> String? { - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - return keychain.get("AccessToken_\(userID)") - } - - func doesUserHaveSavedSession(userID: String) -> Bool { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - - if savedUsers!.isEmpty { - return false - } - - return true - } - - func getSavedSession(userID: String) -> SignedInUser { - let savedUserRequest: NSFetchRequest = SignedInUser.fetchRequest() - savedUserRequest.predicate = NSPredicate(format: "user_id == %@", userID) - let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) - return savedUsers!.first! - } - - func loginWithSavedSession(user: SignedInUser) { - let accessToken = getAuthToken(userID: user.user_id!) - userDefaults.set(user.user_id!, forKey: "lastUsedUserID") - self.user = user - generateAuthHeader(with: accessToken, deviceID: user.device_uuid) - print(JellyfinAPI.customHeaders) - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignIn"), object: nil) - } - - func login(username: String, password: String) -> AnyPublisher { - generateAuthHeader(with: nil, deviceID: nil) - - return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) - .map { response -> (SignedInUser, String?) in - let user = SignedInUser(context: PersistenceController.shared.container.viewContext) - user.username = response.user?.name - user.user_id = response.user?.id - user.device_uuid = self.deviceID - - #if os(tvOS) - let descriptor: TVAppProfileDescriptor = TVAppProfileDescriptor(name: user.username!) - self.tvUserManager.shouldStorePreferenceForCurrentUser(to: descriptor) { should in - if should { - user.appletv_id = self.tvUserManager.currentUserIdentifier ?? "" - } - } - #endif - - return (user, response.accessToken) - } - .handleEvents(receiveOutput: { [unowned self] response, accessToken in - user = response - _ = try? PersistenceController.shared.container.viewContext.save() - - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)") - - generateAuthHeader(with: accessToken, deviceID: user.device_uuid) - - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignIn"), object: nil) - }) - .map(\.0) - .eraseToAnyPublisher() - } - - func logout() { - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" - keychain.delete("AccessToken_\(user?.user_id ?? "")") - generateAuthHeader(with: nil, deviceID: nil) - if user != nil { - let deleteRequest = NSBatchDeleteRequest(objectIDs: [user.objectID]) - user = nil - _ = try? PersistenceController.shared.container.viewContext.execute(deleteRequest) - } - } } diff --git a/Shared/Singleton/SwiftfinNotificationCenter.swift b/Shared/Singleton/SwiftfinNotificationCenter.swift new file mode 100644 index 00000000..c20973a2 --- /dev/null +++ b/Shared/Singleton/SwiftfinNotificationCenter.swift @@ -0,0 +1,24 @@ +// + /* + * 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 + +enum SwiftfinNotificationCenter { + + static let main: NotificationCenter = { + return NotificationCenter() + }() + + enum Keys { + static let didSignIn = Notification.Name("didSignIn") + static let didSignOut = Notification.Name("didSignOut") + static let processDeepLink = Notification.Name("processDeepLink") + static let didPurge = Notification.Name("didPurge") + } +} diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift new file mode 100644 index 00000000..1711e9cb --- /dev/null +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -0,0 +1,183 @@ +// + /* + * 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 CoreStore +import Defaults + +enum SwiftfinStore { + + // MARK: State + // Safe, copyable representations of their underlying CoreStoredObject's + // Relationships are represented by the related object's IDs or value + enum State { + + struct Server { + let uri: String + let name: String + let id: String + let os: String + let version: String + let userIDs: [String] + + fileprivate init(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { + self.uri = uri + self.name = name + self.id = id + self.os = os + self.version = version + self.userIDs = usersIDs + } + + static var sample: Server { + return Server(uri: "https://www.notaurl.com", name: "Johnny's Tree", id: "123abc", os: "macOS", version: "1.1.1", usersIDs: ["1", "2"]) + } + } + + struct User { + let username: String + let id: String + let serverID: String + let accessToken: String + + fileprivate init(username: String, id: String, serverID: String, accessToken: String) { + self.username = username + self.id = id + self.serverID = serverID + self.accessToken = accessToken + } + + static var sample: User { + return User(username: "JohnnyAppleseed", id: "123abc", serverID: "123abc", accessToken: "open-sesame") + } + } + } + + // MARK: Models + enum Models { + + final class StoredServer: CoreStoreObject { + + @Field.Stored("uri") + var uri: String = "" + + @Field.Stored("name") + var name: String = "" + + @Field.Stored("id") + var id: String = "" + + @Field.Stored("os") + var os: String = "" + + @Field.Stored("version") + var version: String = "" + + @Field.Relationship("users", inverse: \StoredUser.$server) + var users: Set + + var state: State.Server { + return State.Server(uri: uri, + name: name, + id: id, + os: os, + version: version, + usersIDs: users.map({ $0.id })) + } + } + + final class StoredUser: CoreStoreObject { + + @Field.Stored("username") + var username: String = "" + + @Field.Stored("id") + var id: String = "" + + @Field.Stored("appleTVID") + var appleTVID: String = "" + + @Field.Relationship("server") + var server: StoredServer? + + @Field.Relationship("accessToken", inverse: \StoredAccessToken.$user) + var accessToken: StoredAccessToken? + + var state: State.User { + guard let server = server else { fatalError("No server associated with user") } + guard let accessToken = accessToken else { fatalError("No access token associated with user") } + return State.User(username: username, + id: id, + serverID: server.id, + accessToken: accessToken.value) + } + } + + final class StoredAccessToken: CoreStoreObject { + + @Field.Stored("value") + var value: String = "" + + @Field.Relationship("user") + var user: StoredUser? + } + } + + // MARK: Errors + enum Errors { + case existingServer(State.Server) + case existingUser(State.User) + } + + // MARK: dataStack + static let dataStack: DataStack = { + let schema = CoreStoreSchema(modelVersion: "V1", + entities: [ + Entity("Server"), + Entity("User"), + Entity("AccessToken") + ], + versionLock: [ + "AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], + "Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db], + "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] + ]) + + let _dataStack = DataStack(schema) + try! _dataStack.addStorageAndWait( + SQLiteStore( + fileName: "Swiftfin.sqlite", + localStorageOptions: .recreateStoreOnModelMismatch + ) + ) + return _dataStack + }() +} + +// MARK: LocalizedError +extension SwiftfinStore.Errors: LocalizedError { + + var title: String { + switch self { + case .existingServer(_): + return "Existing Server" + case .existingUser(_): + return "Existing User" + } + } + + var errorDescription: String? { + switch self { + case .existingServer(let server): + return "Server \(server.name) already exists with same server ID" + case .existingUser(let user): + return "User \(user.username) already exists with same user ID" + } + } +} diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift new file mode 100644 index 00000000..ab6a28d0 --- /dev/null +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -0,0 +1,35 @@ +// + /* + * 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 Defaults +import Foundation + +extension SwiftfinStore { + + enum Defaults { + + static let suite: UserDefaults = { + return UserDefaults(suiteName: "swiftfinstore-defaults")! + }() + } +} + +extension Defaults.Keys { + static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.suite) + + static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite) + static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) + static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) + static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.suite) + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite) + static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.suite) + static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.suite) + static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .thirty, suite: SwiftfinStore.Defaults.suite) + static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .thirty, suite: SwiftfinStore.Defaults.suite) +} diff --git a/Shared/ViewModels/BasicAppSettingsViewModel.swift b/Shared/ViewModels/BasicAppSettingsViewModel.swift new file mode 100644 index 00000000..ca3d477e --- /dev/null +++ b/Shared/ViewModels/BasicAppSettingsViewModel.swift @@ -0,0 +1,19 @@ +// + /* + * 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 + +final class BasicAppSettingsViewModel: ViewModel { + + let appearances = AppAppearance.allCases + + func reset() { + SessionManager.main.purge() + } +} diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 04929d81..2f4b1460 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -13,106 +13,63 @@ import JellyfinAPI import Stinsen final class ConnectToServerViewModel: ViewModel { - @RouterObject - var main: MainCoordinator.Router? - - @Published var isConnectedServer = false - - var uriSubject = CurrentValueSubject("") - var usernameSubject = CurrentValueSubject("") - var passwordSubject = CurrentValueSubject("") - - @Published var lastPublicUsers = [UserDto]() - @Published var publicUsers = [UserDto]() - @Published var selectedPublicUser = UserDto() - - private let discovery = ServerDiscovery() - @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] + + @RouterObject var router: ConnectToServerCoodinator.Router? + @Published var discoveredServers: Set = [] @Published var searching = false - - func getPublicUsers() { - if ServerEnvironment.current.server != nil { - LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)", - tag: "getPublicUsers") - UserAPI.getPublicUsers() - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { response in - self.publicUsers = response - LogManager.shared.log.debug("Received \(String(response.count)) public users.", tag: "getPublicUsers") - self.isConnectedServer = true - }) - .store(in: &cancellables) - } else { - LogManager.shared.log.debug("Not getting users - server is nil", tag: "getPublicUsers") + private let discovery = ServerDiscovery() + + var alertTitle: String { + var message: String = "" + if errorMessage?.code != ErrorMessage.noShowErrorCode { + message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") } + message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") + return message } - func hidePublicUsers() { - lastPublicUsers = publicUsers - publicUsers = [] - } - - func showPublicUsers() { - publicUsers = lastPublicUsers - lastPublicUsers = [] - } - - func connectToServer() { + func connectToServer(uri: String) { #if targetEnvironment(simulator) - if uriSubject.value == "localhost" { - uriSubject.value = "http://localhost:8096" - } + var uri = uri + if uri == "localhost" { + uri = "http://localhost:8096" + } #endif - LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer") - ServerEnvironment.current.create(with: uriSubject.value) + LogManager.shared.log.debug("Attempting to connect to server at \"\(uri)\"", tag: "connectToServer") + SessionManager.main.connectToServer(with: uri) .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) - }, receiveValue: { _ in - LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer") - self.getPublicUsers() + }, receiveValue: { server in + LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer") + self.router?.route(to: \.userSignIn, server) }) .store(in: &cancellables) } - func connectToServer(at url: URL) { - uriSubject.send(url.absoluteString) - connectToServer() - } - func discoverServers() { + discoveredServers.removeAll() searching = true - // Timeout after 5 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + // Timeout after 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.searching = false } discovery.locateServer { [self] server in - if let server = server, !servers.contains(server) { - servers.append(server) + if let server = server { + discoveredServers.insert(server) } - searching = false } } - - func login() { - LogManager.shared.log.debug("Attempting to login to server at \"\(uriSubject.value)\"", tag: "login") - LogManager.shared.log - .debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)", - tag: "login") - SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", - completion: completion) - }, receiveValue: { [weak self] _ in - self?.main?.root(\.mainTab) - }) - .store(in: &cancellables) + + func cancelConnection() { + for cancellable in cancellables { + cancellable.cancel() + } + + self.isLoading = false } } diff --git a/Shared/ViewModels/EpisodeItemViewModel.swift b/Shared/ViewModels/EpisodeItemViewModel.swift index eaed1330..89a88ca2 100644 --- a/Shared/ViewModels/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/EpisodeItemViewModel.swift @@ -26,7 +26,7 @@ final class EpisodeItemViewModel: ItemViewModel { func routeToSeasonItem() { guard let id = item.seasonId else { return } - UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -38,7 +38,7 @@ final class EpisodeItemViewModel: ItemViewModel { func routeToSeriesItem() { guard let id = item.seriesId else { return } - UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 0f98e7a4..bd2c1e24 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -14,10 +14,10 @@ import JellyfinAPI final class HomeViewModel: ViewModel { - @Published var librariesShowRecentlyAddedIDs = [String]() - @Published var libraries = [BaseItemDto]() - @Published var resumeItems = [BaseItemDto]() - @Published var nextUpItems = [BaseItemDto]() + @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: [.dateAdded]) @@ -29,53 +29,83 @@ final class HomeViewModel: ViewModel { func refresh() { LogManager.shared.log.debug("Refresh called.") - UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id!) + UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) + switch completion { + case .finished: () + case .failure(_): + self.libraries = [] + self.handleAPIRequestError(completion: completion) + } }, receiveValue: { response in + + var newLibraries: [BaseItemDto] = [] + response.items!.forEach { item in LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") if item.collectionType == "movies" || item.collectionType == "tvshows" { - self.libraries.append(item) + newLibraries.append(item) } } UserAPI.getCurrentUser() .trackActivity(self.loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) + switch completion { + case .finished: () + case .failure(_): + self.libraries = [] + self.handleAPIRequestError(completion: completion) + } }, receiveValue: { response in - self.libraries.forEach { library in - if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! { - LogManager.shared.log.debug("Adding library \(library.id!) (\(library.name ?? "nil")) to recently added list") - self.librariesShowRecentlyAddedIDs.append(library.id!) + let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : [] + + for excludeID in excludeIDs { + newLibraries.removeAll { library in + return library.id == excludeID } } + + self.libraries = newLibraries }) .store(in: &self.cancellables) }) .store(in: &cancellables) - ItemsAPI.getResumeItems(userId: SessionManager.current.user.user_id!, limit: 12, + ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + mediaTypes: ["Video"], + imageTypeLimit: 1, + enableImageTypes: [.primary, .backdrop, .thumb]) .trackActivity(loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) + switch completion { + case .finished: () + case .failure(_): + self.resumeItems = [] + self.handleAPIRequestError(completion: completion) + } }, receiveValue: { response in LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") + self.resumeItems = response.items ?? [] }) .store(in: &cancellables) - TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, limit: 12, + TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) + switch completion { + case .finished: () + case .failure(_): + self.nextUpItems = [] + self.handleAPIRequestError(completion: completion) + } }, receiveValue: { response in LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") + self.nextUpItems = response.items ?? [] }) .store(in: &cancellables) diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel.swift index f55b3171..7b2dc5c3 100644 --- a/Shared/ViewModels/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel.swift @@ -47,7 +47,7 @@ class ItemViewModel: ViewModel { } func getSimilarItems() { - LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -59,7 +59,7 @@ class ItemViewModel: ViewModel { func updateWatchState() { if isWatched { - PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -68,7 +68,7 @@ class ItemViewModel: ViewModel { }) .store(in: &cancellables) } else { - PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -81,7 +81,7 @@ class ItemViewModel: ViewModel { func updateFavoriteState() { if isFavorited { - UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -90,7 +90,7 @@ class ItemViewModel: ViewModel { }) .store(in: &cancellables) } else { - UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!) + UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift index d0ebbba8..cca5b38e 100644 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ b/Shared/ViewModels/LatestMediaViewModel.swift @@ -25,8 +25,8 @@ final class LatestMediaViewModel: ViewModel { } func requestLatestMedia() { - LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")") - UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, + LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)") + UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: libraryID, fields: [ .primaryImageAspectRatio, diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index 30cc442e..127e4d99 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -58,7 +58,7 @@ final class LibraryFilterViewModel: ViewModel { } func requestQueryFilters() { - FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!, parentId: self.parentId) + FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift index 1c85db9e..a95c429f 100644 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ b/Shared/ViewModels/LibraryListViewModel.swift @@ -24,7 +24,7 @@ final class LibraryListViewModel: ViewModel { } func requestLibraries() { - UserViewsAPI.getUserViews(userId: SessionManager.current.user.user_id ?? "val was nil") + UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) diff --git a/Shared/ViewModels/LibrarySearchViewModel.swift b/Shared/ViewModels/LibrarySearchViewModel.swift index 9d0e035b..0aeb6098 100644 --- a/Shared/ViewModels/LibrarySearchViewModel.swift +++ b/Shared/ViewModels/LibrarySearchViewModel.swift @@ -77,7 +77,7 @@ final class LibrarySearchViewModel: ViewModel { } func requestSuggestions() { - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 20, recursive: true, parentId: parentID, @@ -96,7 +96,7 @@ final class LibrarySearchViewModel: ViewModel { } func search(with query: String) { - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) @@ -107,7 +107,7 @@ final class LibrarySearchViewModel: ViewModel { self?.movieItems = response.items ?? [] }) .store(in: &cancellables) - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) @@ -118,7 +118,7 @@ final class LibrarySearchViewModel: ViewModel { self?.showItems = response.items ?? [] }) .store(in: &cancellables) - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 7a969f24..f0839708 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -15,9 +15,9 @@ import SwiftUICollection typealias LibraryRow = CollectionRow struct LibraryRowCell: Hashable { - let id = UUID() - let item: BaseItemDto? - var loadingCell: Bool = false + let id = UUID() + let item: BaseItemDto? + var loadingCell: Bool = false } final class LibraryViewModel: ViewModel { @@ -38,6 +38,7 @@ final class LibraryViewModel: ViewModel { @Published var filters: LibraryFilters private let columns: Int + private var libraries = [BaseItemDto]() var enabledFilterType: [FilterType] { if genre == nil { @@ -48,12 +49,12 @@ final class LibraryViewModel: ViewModel { } init( - parentID: String? = nil, - person: BaseItemPerson? = nil, - genre: NameGuidPair? = nil, - studio: NameGuidPair? = nil, - filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), - columns: Int = 7 + parentID: String? = nil, + person: BaseItemPerson? = nil, + genre: NameGuidPair? = nil, + studio: NameGuidPair? = nil, + filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), + columns: Int = 7 ) { self.parentID = parentID self.person = person @@ -63,9 +64,11 @@ final class LibraryViewModel: ViewModel { self.columns = columns super.init() + $filters .sink(receiveValue: requestItems(with:)) .store(in: &cancellables) + } func requestItems(with filters: LibraryFilters) { @@ -79,7 +82,7 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) let shouldBeRecursive: Bool = filters.filters.contains(.isFavorite) || personIDs != [] || studioIDs != [] || genreIDs != [] - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, @@ -95,7 +98,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items = response.items ?? [] - self.rows = self.calculateRows() + self.rows = self.calculateRows(for: self.items) }) .store(in: &cancellables) } @@ -111,7 +114,7 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) let shouldBeRecursive: Bool = filters.filters.contains(.isFavorite) || personIDs != [] || studioIDs != [] || genreIDs != [] - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, recursive: shouldBeRecursive, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, @@ -125,7 +128,7 @@ final class LibraryViewModel: ViewModel { self.hasPreviousPage = self.currentPage > 0 self.hasNextPage = self.currentPage < self.totalPages - 1 self.items.append(contentsOf: response.items ?? []) - self.rows = self.calculateRows() + self.rows = self.calculateRows(for: self.items) }) .store(in: &cancellables) } @@ -145,37 +148,35 @@ final class LibraryViewModel: ViewModel { requestItems(with: filters) } - private func calculateRows() -> [LibraryRow] { - guard items.count > 0 else { return [] } - let rowCount = items.count / columns - var calculatedRows = [LibraryRow]() - for i in (0...rowCount) { - - let firstItemIndex = i * columns - var lastItemIndex = firstItemIndex + columns - if lastItemIndex > items.count { - lastItemIndex = items.count - } - - var rowCells = [LibraryRowCell]() - for item in items[firstItemIndex.. [LibraryRow] { + guard itemList.count > 0 else { return [] } + let rowCount = itemList.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > itemList.count { + lastItemIndex = itemList.count + } + + var rowCells = [LibraryRowCell]() + for item in itemList[firstItemIndex.. [LibraryRow] { + guard libraries.count > 0 else { return [] } + let rowCount = libraries.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > libraries.count { + lastItemIndex = libraries.count + } + + var rowCells = [LibraryRowCell]() + for item in libraries[firstItemIndex.. String { + if server.userIDs.count == 1 { + return "1 user" + } else { + return "\(server.userIDs.count) users" + } + } + + func remove(server: SwiftfinStore.State.Server) { + SessionManager.main.delete(server: server) + fetchServers() + } + + @objc private func didPurge() { + fetchServers() + } +} diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 86f6a56a..5cedb82f 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -11,48 +11,6 @@ import Foundation import SwiftUI import Defaults -struct UserSettings: Decodable { - var LocalMaxBitrate: Int - var RemoteMaxBitrate: Int - var AutoSelectSubtitles: Bool - var AutoSelectSubtitlesLangcode: String - var SubtitlePositionOffset: Int - var SubtitleFontName: String -} - -struct Bitrates: Codable, Hashable { - public var name: String - public var value: Int -} - -struct TrackLanguage: Hashable { - var name: String - var isoCode: String - - static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") -} - -enum AppAppearance: String, CaseIterable, Defaults.Serializable { - case system - case dark - case light - - var localizedName: String { - return NSLocalizedString(self.rawValue.capitalized, comment: "") - } - - var style: UIUserInterfaceStyle { - switch self { - case .system: - return .unspecified - case .dark: - return .dark - case .light: - return .light - } - } -} - final class SettingsViewModel: ObservableObject { let currentLocale = Locale.current var bitrates: [Bitrates] = [] diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift deleted file mode 100644 index df524bb1..00000000 --- a/Shared/ViewModels/SplashViewModel.swift +++ /dev/null @@ -1,49 +0,0 @@ -// - /* - * 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 UIKit - -#if !os(tvOS) -import WidgetKit -#endif - -final class SplashViewModel: ViewModel { - - @Published var isLoggedIn: Bool = false - - 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 - - #if !os(tvOS) - WidgetCenter.shared.reloadAllTimelines() - UIScrollView.appearance().keyboardDismissMode = .onDrag - #endif - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil) - nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil) - } - - @objc func didLogIn() { - LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") - isLoggedIn = true - } - - @objc func didLogOut() { - LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") - isLoggedIn = false - } -} diff --git a/Shared/ViewModels/TVLibrariesViewModel.swift b/Shared/ViewModels/TVLibrariesViewModel.swift new file mode 100644 index 00000000..6d5a5ef4 --- /dev/null +++ b/Shared/ViewModels/TVLibrariesViewModel.swift @@ -0,0 +1,95 @@ +// +/* + * 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 +import Stinsen +import SwiftUICollection + +final class TVLibrariesViewModel: ViewModel { + + @Published var rows = [LibraryRow]() + @Published var totalPages = 0 + @Published var currentPage = 0 + @Published var hasNextPage = false + @Published var hasPreviousPage = false + + private var libraries = [BaseItemDto]() + private let columns: Int + + @RouterObject + var router: TVLibrariesCoordinator.Router? + + init( + columns: Int = 7 + ) { + self.columns = columns + super.init() + + requestLibraries() + } + + func requestLibraries() { + + UserViewsAPI.getUserViews( + userId: SessionManager.main.currentLogin.user.id) + .trackActivity(loading) + .sink(receiveCompletion: { completion in + self.handleAPIRequestError(completion: completion) + }, receiveValue: { response in + if let responseItems = response.items { + self.libraries = [] + for library in responseItems { + if library.collectionType == "tvshows" { + self.libraries.append(library) + } + } + self.rows = self.calculateRows() + if self.libraries.count == 1, let library = self.libraries.first { + // show library + self.router?.route(to: \.library, library) + } + } + }) + .store(in: &cancellables) + } + + private func calculateRows() -> [LibraryRow] { + guard libraries.count > 0 else { return [] } + let rowCount = libraries.count / columns + var calculatedRows = [LibraryRow]() + for i in (0...rowCount) { + let firstItemIndex = i * columns + var lastItemIndex = firstItemIndex + columns + if lastItemIndex > libraries.count { + lastItemIndex = libraries.count + } + + var rowCells = [LibraryRowCell]() + for item in libraries[firstItemIndex.. Void) { + + guard let currentLogin = SessionManager.main.currentLogin else { return } + let currentDate = Date() - let server = ServerEnvironment.current.server - let savedUser = SessionManager.current.user + let server = currentLogin.server + let savedUser = currentLogin.user var tempCancellables = Set() - if server != nil && savedUser != nil { - JellyfinAPI.basePath = server!.baseURI ?? "" - TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(NextUpEntry(date: currentDate, items: [], error: error)) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) + JellyfinAPI.basePath = server.uri + TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(NextUpEntry(date: currentDate, items: [], error: error)) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in + guard case let .success(image) = result else { dispatchGroup.leave() + return } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() } + } - dispatchGroup.notify(queue: .main) { - completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) - } - }) - .store(in: &tempCancellables) - } + dispatchGroup.notify(queue: .main) { + completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) + } + }) + .store(in: &tempCancellables) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + + guard let currentLogin = SessionManager.main.currentLogin else { return } + let currentDate = Date() let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! - let server = ServerEnvironment.current.server - let savedUser = SessionManager.current.user + let server = currentLogin.server + let savedUser = currentLogin.user var tempCancellables = Set() - if server != nil && savedUser != nil { - JellyfinAPI.basePath = server!.baseURI ?? "" - TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sink(receiveCompletion: { result in - switch result { - case .finished: - break - case let .failure(error): - completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) - } - }, receiveValue: { response in - let dispatchGroup = DispatchGroup() - let items = response.items ?? [] - var downloadedItems = [(BaseItemDto, UIImage?)]() - items.enumerated().forEach { _, item in - dispatchGroup.enter() - ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in - guard case let .success(image) = result else { - dispatchGroup.leave() - return - } - downloadedItems.append((item, image.image)) + JellyfinAPI.basePath = server.uri + TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(maxWidth: 320)) { result in + guard case let .success(image) = result else { dispatchGroup.leave() + return } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() } + } - dispatchGroup.notify(queue: .main) { - completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], - policy: .after(entryDate))) - } - }) - .store(in: &tempCancellables) - } + dispatchGroup.notify(queue: .main) { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], + policy: .after(entryDate))) + } + }) + .store(in: &tempCancellables) } } @@ -198,7 +200,8 @@ extension NextUpEntryView { } func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! + return Link(destination: url, label: { VStack(alignment: .leading) { if let image = item.1 { Image(uiImage: image) @@ -223,7 +226,8 @@ extension NextUpEntryView { } func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(item.0.id!)")! + return Link(destination: url, label: { HStack(spacing: 20) { if let image = item.1 { Image(uiImage: image) @@ -285,7 +289,8 @@ extension NextUpEntryView { func large(items: [(BaseItemDto, UIImage?)]) -> some View { VStack(spacing: 0) { if let firstItem = items[safe: 0] { - Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(firstItem.0.id!)")!, + let url = URL(string: "widget-extension://Users/\(SessionManager.main.currentLogin.user.id)/Items/\(firstItem.0.id!)")! + Link(destination: url, label: { ZStack(alignment: .topTrailing) { ZStack(alignment: .bottomLeading) { diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements deleted file mode 100644 index b164e1cb..00000000 --- a/WidgetExtension/WidgetExtension.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.application-groups - - group.me.vigue.jellyfin.mobileclient - - keychain-access-groups - - $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain - - -