Merge pull request #181 from LePips/multi-server-user-login
Multi Server/User Support and More
This commit is contained in:
commit
5a7ec2463f
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,12 @@
|
|||
//
|
||||
/*
|
||||
* 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 PlainNavigationLinkButtonStyle: ButtonStyle {
|
|
@ -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 {
|
||||
|
|
|
@ -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: ""))
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.user-management</key>
|
||||
<array>
|
||||
<string>get-current-user</string>
|
||||
<string>runs-as-current-user</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -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!
|
|
@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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<LibraryFilters>, 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -91,15 +91,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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 ?? "")
|
File diff suppressed because it is too large
Load Diff
|
@ -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": "6bde3b0063ba8e7537b43744948535ca7e9e0dad",
|
||||
"version": "0.5.2"
|
||||
"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": "8a6e4a96fd38504a05903d136c85634b65fd7c4d",
|
||||
"version": "6.0.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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 <swiftfin-bugs@jellyfin.org>"])
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/* 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 {
|
||||
// TODO: Replace with a SplashView
|
||||
Color(appAppearance.style == .light ? UIColor.white : UIColor.black)
|
||||
.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))
|
||||
}
|
||||
}
|
|
@ -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<AnyView> {
|
||||
init<V: View>(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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -9,10 +9,6 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
protocol PillStackable {
|
||||
var title: String { get }
|
||||
}
|
||||
|
||||
struct PillHStackView<ItemType: PillStackable>: View {
|
||||
|
||||
let title: String
|
||||
|
|
|
@ -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<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||
|
||||
let items: [ItemType]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<VideoPlayerCoordinator> {
|
||||
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<ItemCoordinator>(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
|
|
@ -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<MainCoordinator>
|
||||
|
||||
@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<ConnectToServerCoodinator> {
|
||||
NavigationViewCoordinator(ConnectToServerCoodinator())
|
||||
}
|
||||
}
|
||||
|
||||
#elseif os(tvOS)
|
||||
// temp for fixing build error
|
||||
final class MainCoordinator: NavigationCoordinatable {
|
||||
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
|
||||
|
||||
@Root var mainTab = makeEmpty
|
||||
|
||||
@ViewBuilder func makeEmpty() -> some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -2,19 +2,9 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.coremedia.hls.low-latency</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.me.vigue.jellyfin.mobileclient</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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<AnyView> {
|
||||
init<V: View>(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 <swiftfin-bugs@jellyfin.org>"])
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
})
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
basicAppSettingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -11,17 +11,10 @@ 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 {
|
||||
|
@ -73,7 +66,7 @@ struct HomeView: View {
|
|||
Button {
|
||||
homeRouter.route(to: \.settings)
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
Image(systemName: "gearshape.fill")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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 = ""
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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!,
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -14,7 +14,9 @@ import SwiftUI
|
|||
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||
|
||||
final class FilterCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
@Binding var filters: LibraryFilters
|
|
@ -13,6 +13,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class HomeCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
|
@ -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<VideoPlayerCoordinator> {
|
||||
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ItemNavigationView(item: itemDto)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import SwiftUI
|
|||
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
||||
|
||||
final class LibraryCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
@ -22,8 +23,8 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
|||
@Route(.modal) var filter = makeFilter
|
||||
@Route(.push) var item = makeItem
|
||||
|
||||
var viewModel: LibraryViewModel
|
||||
var title: String
|
||||
let viewModel: LibraryViewModel
|
||||
let title: String
|
||||
|
||||
init(viewModel: LibraryViewModel, title: String) {
|
||||
self.viewModel = viewModel
|
|
@ -12,6 +12,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class LibraryListCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
|
@ -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<MainCoordinator>
|
||||
|
||||
@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<ServerListCoordinator> {
|
||||
NavigationViewCoordinator(ServerListCoordinator())
|
||||
}
|
||||
}
|
|
@ -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<HomeCoordinator> {
|
||||
return NavigationViewCoordinator(HomeCoordinator())
|
||||
|
@ -30,11 +29,11 @@ final class MainTabCoordinator: TabCoordinatable {
|
|||
Text("Home")
|
||||
}
|
||||
|
||||
func makeTodos() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
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()
|
||||
}
|
|
@ -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<MainCoordinator>(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<ServerListCoordinator> {
|
||||
NavigationViewCoordinator(ServerListCoordinator())
|
||||
}
|
||||
}
|
|
@ -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 Foundation
|
||||
import SwiftUI
|
||||
import Stinsen
|
||||
|
||||
final class MainTabCoordinator: TabCoordinatable {
|
||||
var child = TabChild(startingItems: [
|
||||
\MainTabCoordinator.home,
|
||||
\MainTabCoordinator.allMedia,
|
||||
\MainTabCoordinator.settings
|
||||
])
|
||||
|
||||
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||
@Route(tabItem: makeAllMediaTab) var allMedia = makeAllMedia
|
||||
@Route(tabItem: makeSettingsTab) var settings = makeSettings
|
||||
|
||||
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
||||
return NavigationViewCoordinator(HomeCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "house")
|
||||
Text("Home")
|
||||
}
|
||||
}
|
||||
|
||||
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
return NavigationViewCoordinator(LibraryListCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "folder")
|
||||
Text("All Media")
|
||||
}
|
||||
}
|
||||
|
||||
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
||||
return NavigationViewCoordinator(SettingsCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "gearshape.fill")
|
||||
Text("Settings")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<BasicAppSettingsCoordinator> {
|
||||
NavigationViewCoordinator(BasicAppSettingsCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ServerListView(viewModel: ServerListViewModel())
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
final class SettingsCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue