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 @@
-
-
-
-