commit
ae41058a76
|
@ -19,7 +19,7 @@ struct PublicUserButton: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if publicUser.primaryImageTag != nil {
|
if publicUser.primaryImageTag != nil {
|
||||||
ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||||
.frame(width: 250, height: 250)
|
.frame(width: 250, height: 250)
|
||||||
.cornerRadius(125.0)
|
.cornerRadius(125.0)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerDetailView: View {
|
struct ServerDetailView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = ServerDetailViewModel()
|
@ObservedObject var viewModel: ServerDetailViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -26,7 +26,7 @@ struct ServerDetailView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("URI")
|
Text("URI")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(SessionManager.main.currentLogin.server.uri)
|
Text(SessionManager.main.currentLogin.server.currentURI)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,19 +44,6 @@ struct ServerDetailView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
viewModel.refreshServerLibrary()
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Text("Refresh Library")
|
|
||||||
.font(.callout)
|
|
||||||
Spacer()
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).disabled(viewModel.isLoading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct ServerListView: View {
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Text(server.uri)
|
Text(server.currentURI)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -164,13 +164,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
// Item is being transcoded by request of server
|
// Item is being transcoded by request of server
|
||||||
if let transcodiungUrl = mediaSource.transcodingUrl {
|
if let transcodiungUrl = mediaSource.transcodingUrl {
|
||||||
item.videoType = .transcode
|
item.videoType = .transcode
|
||||||
streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(transcodiungUrl)")!
|
streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(transcodiungUrl)")!
|
||||||
}
|
}
|
||||||
// Item will be directly played by the client
|
// Item will be directly played by the client
|
||||||
else {
|
else {
|
||||||
item.videoType = .directPlay
|
item.videoType = .directPlay
|
||||||
// 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.currentURI)/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 ?? "")")!
|
streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")!
|
||||||
}
|
}
|
||||||
|
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
|
@ -185,7 +185,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
var deliveryUrl: URL?
|
var deliveryUrl: URL?
|
||||||
|
|
||||||
if stream.deliveryMethod == .external {
|
if stream.deliveryMethod == .external {
|
||||||
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")!
|
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(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 ?? "")
|
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
|
||||||
|
|
|
@ -247,6 +247,8 @@
|
||||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||||
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||||
|
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
|
||||||
|
E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
|
||||||
E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; };
|
E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; };
|
||||||
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; };
|
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; };
|
||||||
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; };
|
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; };
|
||||||
|
@ -565,6 +567,7 @@
|
||||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||||
|
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
||||||
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
|
||||||
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
|
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
|
||||||
E13DD3BC27163C63009D4DAF /* EmailHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailHelper.swift; sourceTree = "<group>"; };
|
E13DD3BC27163C63009D4DAF /* EmailHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailHelper.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1097,19 +1100,20 @@
|
||||||
62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = {
|
62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */,
|
|
||||||
E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */,
|
E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */,
|
||||||
|
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */,
|
||||||
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
||||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
|
||||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||||
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
||||||
|
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
||||||
C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */,
|
|
||||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||||
|
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */,
|
||||||
E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */,
|
E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */,
|
||||||
6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */,
|
6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */,
|
||||||
|
C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */,
|
||||||
E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */,
|
E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */,
|
||||||
E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */,
|
E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */,
|
||||||
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */,
|
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */,
|
||||||
|
@ -1770,6 +1774,7 @@
|
||||||
53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */,
|
53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */,
|
||||||
E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
||||||
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */,
|
||||||
|
E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||||
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
||||||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||||
|
@ -1924,6 +1929,7 @@
|
||||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||||
|
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||||
|
|
|
@ -63,12 +63,12 @@ network.</string>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIImageRespectsSafeAreaInsets</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIImageName</key>
|
|
||||||
<string>swiftfin-logo</string>
|
|
||||||
<key>UIColorName</key>
|
<key>UIColorName</key>
|
||||||
<string>LaunchScreenBackground</string>
|
<string>LaunchScreenBackground</string>
|
||||||
|
<key>UIImageName</key>
|
||||||
|
<string>swiftfin-logo</string>
|
||||||
|
<key>UIImageRespectsSafeAreaInsets</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
|
||||||
@StateObject var viewModel: ConnectToServerViewModel
|
@ObservedObject var viewModel: ConnectToServerViewModel
|
||||||
@State var uri = ""
|
@State var uri = ""
|
||||||
|
|
||||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||||
|
@ -106,6 +106,14 @@ struct ConnectToServerView: View {
|
||||||
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
||||||
dismissButton: .cancel())
|
dismissButton: .cancel())
|
||||||
}
|
}
|
||||||
|
.alert(item: $viewModel.addServerURIPayload) { _ in
|
||||||
|
Alert(title: L10n.existingServer.text,
|
||||||
|
message: L10n.serverAlreadyExistsPrompt(viewModel.addServerURIPayload?.server.name ?? "").text,
|
||||||
|
primaryButton: .default(L10n.addURL.text, action: {
|
||||||
|
viewModel.addURIToServer(addServerURIPayload: viewModel.backAddServerURIPayload!)
|
||||||
|
}),
|
||||||
|
secondaryButton: .cancel())
|
||||||
|
}
|
||||||
.navigationTitle(L10n.connect)
|
.navigationTitle(L10n.connect)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.discoverServers()
|
viewModel.discoverServers()
|
||||||
|
|
|
@ -11,7 +11,13 @@ import SwiftUI
|
||||||
|
|
||||||
struct ServerDetailView: View {
|
struct ServerDetailView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = ServerDetailViewModel()
|
@ObservedObject var viewModel: ServerDetailViewModel
|
||||||
|
@State var currentServerURI: String
|
||||||
|
|
||||||
|
init(viewModel: ServerDetailViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.currentServerURI = viewModel.server.currentURI
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -19,44 +25,33 @@ struct ServerDetailView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Name")
|
Text("Name")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(SessionManager.main.currentLogin.server.name)
|
Text(viewModel.server.name)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
Picker("URI", selection: $currentServerURI) {
|
||||||
Text("URI")
|
ForEach(viewModel.server.uris.sorted(), id: \.self) { uri in
|
||||||
Spacer()
|
Text(uri).tag(uri)
|
||||||
Text(SessionManager.main.currentLogin.server.uri)
|
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
}.onChange(of: currentServerURI) { newValue in
|
||||||
|
viewModel.setServerCurrentURI(uri: newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Version")
|
Text("Version")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(SessionManager.main.currentLogin.server.version)
|
Text(viewModel.server.version)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Operating System")
|
Text("Operating System")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(SessionManager.main.currentLogin.server.os)
|
Text(viewModel.server.os)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
viewModel.refreshServerLibrary()
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Text("Refresh Library")
|
|
||||||
.font(.callout)
|
|
||||||
Spacer()
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).disabled(viewModel.isLoading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ struct ServerListView: View {
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Text(server.uri)
|
Text(server.currentURI)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -90,10 +90,14 @@ struct UserListView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var toolbarContent: some View {
|
private var toolbarContent: some View {
|
||||||
if viewModel.users.isEmpty {
|
|
||||||
EmptyView()
|
|
||||||
} else {
|
|
||||||
HStack {
|
HStack {
|
||||||
|
Button {
|
||||||
|
userListRouter.route(to: \.serverDetail, viewModel.server)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "info.circle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.users.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -549,7 +549,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
let mediaSource = response.mediaSources!.first.self!
|
let mediaSource = response.mediaSources!.first.self!
|
||||||
if mediaSource.transcodingUrl != nil {
|
if mediaSource.transcodingUrl != nil {
|
||||||
// Item is being transcoded by request of server
|
// Item is being transcoded by request of server
|
||||||
let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(mediaSource.transcodingUrl!)")
|
let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(mediaSource.transcodingUrl!)")
|
||||||
let item = PlaybackItem()
|
let item = PlaybackItem()
|
||||||
item.videoType = .transcode
|
item.videoType = .transcode
|
||||||
item.videoUrl = streamURL!
|
item.videoUrl = streamURL!
|
||||||
|
@ -563,7 +563,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
if stream.type == .subtitle {
|
if stream.type == .subtitle {
|
||||||
var deliveryUrl: URL?
|
var deliveryUrl: URL?
|
||||||
if stream.deliveryMethod == .external {
|
if stream.deliveryMethod == .external {
|
||||||
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl ?? "")")!
|
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(stream.deliveryUrl ?? "")")!
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
|
@ -597,8 +597,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
} else {
|
} else {
|
||||||
// TODO: todo
|
// TODO: todo
|
||||||
// Item will be directly played by the client.
|
// Item will be directly played by the client.
|
||||||
let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")!
|
let streamURL = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/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 ?? "")")!
|
// URL(string: "\(SessionManager.main.currentLogin.server.currentURI)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
|
||||||
|
|
||||||
let item = PlaybackItem()
|
let item = PlaybackItem()
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
|
@ -613,7 +613,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
if stream.type == .subtitle {
|
if stream.type == .subtitle {
|
||||||
var deliveryUrl: URL?
|
var deliveryUrl: URL?
|
||||||
if stream.deliveryMethod == .external {
|
if stream.deliveryMethod == .external {
|
||||||
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")!
|
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.currentURI)\(stream.deliveryUrl!)")!
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
|
@ -875,7 +875,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
"userId": SessionManager.main.currentLogin.user.id,
|
"userId": SessionManager.main.currentLogin.user.id,
|
||||||
// "deviceId": SessionManager.main.currentLogin.de.deviceID,
|
// "deviceId": SessionManager.main.currentLogin.de.deviceID,
|
||||||
"accessToken": SessionManager.main.currentLogin.user.accessToken,
|
"accessToken": SessionManager.main.currentLogin.user.accessToken,
|
||||||
"serverAddress": SessionManager.main.currentLogin.server.uri,
|
"serverAddress": SessionManager.main.currentLogin.server.currentURI,
|
||||||
"serverId": SessionManager.main.currentLogin.server.id,
|
"serverId": SessionManager.main.currentLogin.server.id,
|
||||||
"serverVersion": "10.8.0",
|
"serverVersion": "10.8.0",
|
||||||
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
||||||
|
|
|
@ -44,6 +44,7 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
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(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||||
nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil)
|
nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(didChangeServerCurrentURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didLogIn() {
|
@objc func didLogIn() {
|
||||||
|
@ -68,6 +69,15 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func didChangeServerCurrentURI(_ notification: Notification) {
|
||||||
|
guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new current login state server") }
|
||||||
|
guard SessionManager.main.currentLogin != nil else { return }
|
||||||
|
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
|
||||||
|
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeMainTab() -> MainTabCoordinator {
|
func makeMainTab() -> MainTabCoordinator {
|
||||||
MainTabCoordinator()
|
MainTabCoordinator()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ServerDetailCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
|
let stack = NavigationStack(initial: \ServerDetailCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
|
||||||
|
let viewModel: ServerDetailViewModel
|
||||||
|
|
||||||
|
init(viewModel: ServerDetailViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
ServerDetailView(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
@Route(.push) var serverDetail = makeServerDetail
|
@Route(.push) var serverDetail = makeServerDetail
|
||||||
|
|
||||||
@ViewBuilder func makeServerDetail() -> some View {
|
@ViewBuilder func makeServerDetail() -> some View {
|
||||||
ServerDetailView()
|
let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server)
|
||||||
|
ServerDetailView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
|
|
@ -17,6 +17,7 @@ final class UserListCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
@Route(.push) var userSignIn = makeUserSignIn
|
@Route(.push) var userSignIn = makeUserSignIn
|
||||||
|
@Route(.push) var serverDetail = makeServerDetail
|
||||||
|
|
||||||
let viewModel: UserListViewModel
|
let viewModel: UserListViewModel
|
||||||
|
|
||||||
|
@ -28,6 +29,10 @@ final class UserListCoordinator: NavigationCoordinatable {
|
||||||
return UserSignInCoordinator(viewModel: .init(server: server))
|
return UserSignInCoordinator(viewModel: .init(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator {
|
||||||
|
return ServerDetailCoordinator(viewModel: .init(server: server))
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
UserListView(viewModel: viewModel)
|
UserListView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ extension BaseItemPerson {
|
||||||
// MARK: PortraitImageStackable
|
// MARK: PortraitImageStackable
|
||||||
extension BaseItemPerson: PortraitImageStackable {
|
extension BaseItemPerson: PortraitImageStackable {
|
||||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||||
return self.getImage(baseURL: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth)
|
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: String {
|
public var title: String {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Foundation
|
||||||
internal enum L10n {
|
internal enum L10n {
|
||||||
/// Accessibility
|
/// Accessibility
|
||||||
internal static let accessibility = L10n.tr("Localizable", "accessibility")
|
internal static let accessibility = L10n.tr("Localizable", "accessibility")
|
||||||
|
/// Add URL
|
||||||
|
internal static let addURL = L10n.tr("Localizable", "addURL")
|
||||||
/// All Genres
|
/// All Genres
|
||||||
internal static let allGenres = L10n.tr("Localizable", "allGenres")
|
internal static let allGenres = L10n.tr("Localizable", "allGenres")
|
||||||
/// All Media
|
/// All Media
|
||||||
|
@ -56,6 +58,8 @@ internal enum L10n {
|
||||||
internal static let episodes = L10n.tr("Localizable", "episodes")
|
internal static let episodes = L10n.tr("Localizable", "episodes")
|
||||||
/// Error
|
/// Error
|
||||||
internal static let error = L10n.tr("Localizable", "error")
|
internal static let error = L10n.tr("Localizable", "error")
|
||||||
|
/// Existing Server
|
||||||
|
internal static let existingServer = L10n.tr("Localizable", "existingServer")
|
||||||
/// Filter Results
|
/// Filter Results
|
||||||
internal static let filterResults = L10n.tr("Localizable", "filterResults")
|
internal static let filterResults = L10n.tr("Localizable", "filterResults")
|
||||||
/// Filters
|
/// Filters
|
||||||
|
@ -114,7 +118,7 @@ internal enum L10n {
|
||||||
internal static let playNext = L10n.tr("Localizable", "playNext")
|
internal static let playNext = L10n.tr("Localizable", "playNext")
|
||||||
/// Reset
|
/// Reset
|
||||||
internal static let reset = L10n.tr("Localizable", "reset")
|
internal static let reset = L10n.tr("Localizable", "reset")
|
||||||
/// Search...
|
/// Search…
|
||||||
internal static let search = L10n.tr("Localizable", "search")
|
internal static let search = L10n.tr("Localizable", "search")
|
||||||
/// S%1$@:E%2$@
|
/// S%1$@:E%2$@
|
||||||
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
|
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
|
||||||
|
@ -126,6 +130,10 @@ internal enum L10n {
|
||||||
internal static let seeAll = L10n.tr("Localizable", "seeAll")
|
internal static let seeAll = L10n.tr("Localizable", "seeAll")
|
||||||
/// Select Cast Destination
|
/// Select Cast Destination
|
||||||
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
|
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
|
||||||
|
/// Server %s already exists. Add new URL?
|
||||||
|
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
|
||||||
|
return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1)
|
||||||
|
}
|
||||||
/// Server Information
|
/// Server Information
|
||||||
internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
|
internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
|
||||||
/// Server URL
|
/// Server URL
|
||||||
|
@ -150,6 +158,8 @@ internal enum L10n {
|
||||||
internal static let tags = L10n.tr("Localizable", "tags")
|
internal static let tags = L10n.tr("Localizable", "tags")
|
||||||
/// Try again
|
/// Try again
|
||||||
internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
|
internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
|
||||||
|
/// Unknown Error
|
||||||
|
internal static let unknownError = L10n.tr("Localizable", "unknownError")
|
||||||
/// Username
|
/// Username
|
||||||
internal static let username = L10n.tr("Localizable", "username")
|
internal static let username = L10n.tr("Localizable", "username")
|
||||||
/// Who's watching?
|
/// Who's watching?
|
||||||
|
|
|
@ -34,16 +34,12 @@ final class SessionManager {
|
||||||
guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") }
|
guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") }
|
||||||
guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
|
guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
|
||||||
|
|
||||||
JellyfinAPI.basePath = server.uri
|
JellyfinAPI.basePath = server.currentURI
|
||||||
setAuthHeader(with: accessToken.value)
|
setAuthHeader(with: accessToken.value)
|
||||||
currentLogin = (server: existingServer.state, user: user.state)
|
currentLogin = (server: existingServer.state, user: user.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String {
|
|
||||||
return "\(server.id)-\(user.id)"
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchServers() -> [SwiftfinStore.State.Server] {
|
func fetchServers() -> [SwiftfinStore.State.Server] {
|
||||||
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>())
|
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>())
|
||||||
return servers.map({ $0.state })
|
return servers.map({ $0.state })
|
||||||
|
@ -83,7 +79,8 @@ final class SessionManager {
|
||||||
let os = response.operatingSystem,
|
let os = response.operatingSystem,
|
||||||
let version = response.version else { throw JellyfinAPIError("Missing server data from network call") }
|
let version = response.version else { throw JellyfinAPIError("Missing server data from network call") }
|
||||||
|
|
||||||
newServer.uri = uri
|
newServer.uris = [uri]
|
||||||
|
newServer.currentURI = uri
|
||||||
newServer.name = name
|
newServer.name = name
|
||||||
newServer.id = id
|
newServer.id = id
|
||||||
newServer.os = os
|
newServer.os = os
|
||||||
|
@ -107,11 +104,65 @@ final class SessionManager {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
|
||||||
|
return Just(server)
|
||||||
|
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
|
||||||
|
|
||||||
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
|
||||||
|
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else {
|
||||||
|
fatalError("No stored server associated with given state server?")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
|
editServer.uris.insert(uri)
|
||||||
|
|
||||||
|
return (editServer, transaction)
|
||||||
|
}
|
||||||
|
.handleEvents(receiveOutput: { (_, transaction) in
|
||||||
|
try? transaction.commitAndWait()
|
||||||
|
})
|
||||||
|
.map({ (server, _) in
|
||||||
|
return server.state
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
|
||||||
|
return Just(server)
|
||||||
|
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
|
||||||
|
|
||||||
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
|
||||||
|
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else {
|
||||||
|
fatalError("No stored server associated with given state server?")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !existingServer.uris.contains(uri) {
|
||||||
|
fatalError("Attempting to set current uri while server doesn't contain it?")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
|
editServer.currentURI = uri
|
||||||
|
|
||||||
|
return (editServer, transaction)
|
||||||
|
}
|
||||||
|
.handleEvents(receiveOutput: { (_, transaction) in
|
||||||
|
try? transaction.commitAndWait()
|
||||||
|
})
|
||||||
|
.map({ (server, _) in
|
||||||
|
return server.state
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
// Logs in a user with an associated server, storing if successful
|
// Logs in a user with an associated server, storing if successful
|
||||||
func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
setAuthHeader(with: "")
|
setAuthHeader(with: "")
|
||||||
|
|
||||||
JellyfinAPI.basePath = server.uri
|
JellyfinAPI.basePath = server.currentURI
|
||||||
|
|
||||||
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
|
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
|
||||||
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
||||||
|
@ -167,7 +218,7 @@ final class SessionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
||||||
JellyfinAPI.basePath = server.uri
|
JellyfinAPI.basePath = server.currentURI
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
||||||
setAuthHeader(with: user.accessToken)
|
setAuthHeader(with: user.accessToken)
|
||||||
currentLogin = (server: server, user: user)
|
currentLogin = (server: server, user: user)
|
||||||
|
|
|
@ -20,5 +20,6 @@ enum SwiftfinNotificationCenter {
|
||||||
static let didSignOut = Notification.Name("didSignOut")
|
static let didSignOut = Notification.Name("didSignOut")
|
||||||
static let processDeepLink = Notification.Name("processDeepLink")
|
static let processDeepLink = Notification.Name("processDeepLink")
|
||||||
static let didPurge = Notification.Name("didPurge")
|
static let didPurge = Notification.Name("didPurge")
|
||||||
|
static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,22 @@ import Defaults
|
||||||
enum SwiftfinStore {
|
enum SwiftfinStore {
|
||||||
|
|
||||||
// MARK: State
|
// MARK: State
|
||||||
// Safe, copyable representations of their underlying CoreStoredObject's
|
// Safe, copyable representations of their underlying CoreStoredObject
|
||||||
// Relationships are represented by the related object's IDs or value
|
// Relationships are represented by the related object's IDs or value
|
||||||
enum State {
|
enum State {
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
let uri: String
|
let uris: Set<String>
|
||||||
|
let currentURI: String
|
||||||
let name: String
|
let name: String
|
||||||
let id: String
|
let id: String
|
||||||
let os: String
|
let os: String
|
||||||
let version: String
|
let version: String
|
||||||
let userIDs: [String]
|
let userIDs: [String]
|
||||||
|
|
||||||
fileprivate init(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) {
|
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String, usersIDs: [String]) {
|
||||||
self.uri = uri
|
self.uris = uris
|
||||||
|
self.currentURI = currentURI
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self.os = os
|
self.os = os
|
||||||
|
@ -36,7 +38,13 @@ enum SwiftfinStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var sample: Server {
|
static var sample: Server {
|
||||||
return Server(uri: "https://www.notaurl.com", name: "Johnny's Tree", id: "123abc", os: "macOS", version: "1.1.1", usersIDs: ["1", "2"])
|
return Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
|
||||||
|
currentURI: "https://www.notaurl.com",
|
||||||
|
name: "Johnny's Tree",
|
||||||
|
id: "123abc",
|
||||||
|
os: "macOS",
|
||||||
|
version: "1.1.1",
|
||||||
|
usersIDs: ["1", "2"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +62,10 @@ enum SwiftfinStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var sample: User {
|
static var sample: User {
|
||||||
return User(username: "JohnnyAppleseed", id: "123abc", serverID: "123abc", accessToken: "open-sesame")
|
return User(username: "JohnnyAppleseed",
|
||||||
|
id: "123abc",
|
||||||
|
serverID: "123abc",
|
||||||
|
accessToken: "open-sesame")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,8 +75,11 @@ enum SwiftfinStore {
|
||||||
|
|
||||||
final class StoredServer: CoreStoreObject {
|
final class StoredServer: CoreStoreObject {
|
||||||
|
|
||||||
@Field.Stored("uri")
|
@Field.Coded("uris", coder: FieldCoders.Json.self)
|
||||||
var uri: String = ""
|
var uris: Set<String> = []
|
||||||
|
|
||||||
|
@Field.Stored("currentURI")
|
||||||
|
var currentURI: String = ""
|
||||||
|
|
||||||
@Field.Stored("name")
|
@Field.Stored("name")
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
|
@ -83,7 +97,8 @@ enum SwiftfinStore {
|
||||||
var users: Set<StoredUser>
|
var users: Set<StoredUser>
|
||||||
|
|
||||||
var state: State.Server {
|
var state: State.Server {
|
||||||
return State.Server(uri: uri,
|
return State.Server(uris: uris,
|
||||||
|
currentURI: currentURI,
|
||||||
name: name,
|
name: name,
|
||||||
id: id,
|
id: id,
|
||||||
os: os,
|
os: os,
|
||||||
|
@ -145,7 +160,7 @@ enum SwiftfinStore {
|
||||||
],
|
],
|
||||||
versionLock: [
|
versionLock: [
|
||||||
"AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72],
|
"AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72],
|
||||||
"Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db],
|
"Server": [0x936b46acd8e8f0e3, 0x59890d4d9f3f885f, 0x819cf7a4abf98b22, 0xe16125c5af885a06],
|
||||||
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
|
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -165,9 +180,9 @@ extension SwiftfinStore.Errors: LocalizedError {
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .existingServer(_):
|
case .existingServer:
|
||||||
return "Existing Server"
|
return "Existing Server"
|
||||||
case .existingUser(_):
|
case .existingUser:
|
||||||
return "Existing User"
|
return "Existing User"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,24 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
|
struct AddServerURIPayload: Identifiable {
|
||||||
|
|
||||||
|
let server: SwiftfinStore.State.Server
|
||||||
|
let uri: String
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
return server.id.appending(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class ConnectToServerViewModel: ViewModel {
|
final class ConnectToServerViewModel: ViewModel {
|
||||||
|
|
||||||
@RouterObject var router: ConnectToServerCoodinator.Router?
|
@RouterObject var router: ConnectToServerCoodinator.Router?
|
||||||
@Published var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
|
@Published var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
|
||||||
@Published var searching = false
|
@Published var searching = false
|
||||||
|
@Published var addServerURIPayload: AddServerURIPayload?
|
||||||
|
var backAddServerURIPayload: AddServerURIPayload?
|
||||||
|
|
||||||
private let discovery = ServerDiscovery()
|
private let discovery = ServerDiscovery()
|
||||||
|
|
||||||
var alertTitle: String {
|
var alertTitle: String {
|
||||||
|
@ -40,8 +53,26 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
SessionManager.main.connectToServer(with: uri)
|
SessionManager.main.connectToServer(with: uri)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
|
// This is disgusting. ViewModel Error handling overall needs to be refactored
|
||||||
|
switch completion {
|
||||||
|
case .finished: ()
|
||||||
|
case .failure(let error):
|
||||||
|
switch error {
|
||||||
|
case is SwiftfinStore.Errors:
|
||||||
|
let swiftfinError = error as! SwiftfinStore.Errors
|
||||||
|
switch swiftfinError {
|
||||||
|
case .existingServer(let server):
|
||||||
|
self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri)
|
||||||
|
self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri)
|
||||||
|
default:
|
||||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
||||||
completion: completion)
|
completion: completion)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
||||||
|
completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
}, receiveValue: { server in
|
}, receiveValue: { server in
|
||||||
LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer")
|
LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer")
|
||||||
self.router?.route(to: \.userSignIn, server)
|
self.router?.route(to: \.userSignIn, server)
|
||||||
|
@ -65,6 +96,24 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addURIToServer(addServerURIPayload: AddServerURIPayload) {
|
||||||
|
SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri)
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
||||||
|
completion: completion)
|
||||||
|
} receiveValue: { server in
|
||||||
|
SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri)
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
||||||
|
completion: completion)
|
||||||
|
} receiveValue: { _ in
|
||||||
|
self.router?.dismissCoordinator()
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
func cancelConnection() {
|
func cancelConnection() {
|
||||||
for cancellable in cancellables {
|
for cancellable in cancellables {
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
|
|
|
@ -25,6 +25,34 @@ final class HomeViewModel: ViewModel {
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
// Nov. 6, 2021
|
||||||
|
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
|
||||||
|
// See ServerDetailViewModel.swift for feature request issue
|
||||||
|
let nc = SwiftfinNotificationCenter.main
|
||||||
|
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didSignIn() {
|
||||||
|
for cancellable in cancellables {
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
librariesShowRecentlyAddedIDs = []
|
||||||
|
libraries = []
|
||||||
|
resumeItems = []
|
||||||
|
nextUpItems = []
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didSignOut() {
|
||||||
|
for cancellable in cancellables {
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellables.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh() {
|
func refresh() {
|
||||||
|
@ -34,7 +62,7 @@ final class HomeViewModel: ViewModel {
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
case .failure(_):
|
case .failure:
|
||||||
self.libraries = []
|
self.libraries = []
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}
|
}
|
||||||
|
@ -54,7 +82,7 @@ final class HomeViewModel: ViewModel {
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
case .failure(_):
|
case .failure:
|
||||||
self.libraries = []
|
self.libraries = []
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}
|
}
|
||||||
|
@ -82,7 +110,7 @@ final class HomeViewModel: ViewModel {
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
case .failure(_):
|
case .failure:
|
||||||
self.resumeItems = []
|
self.resumeItems = []
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +127,7 @@ final class HomeViewModel: ViewModel {
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished: ()
|
case .finished: ()
|
||||||
case .failure(_):
|
case .failure:
|
||||||
self.nextUpItems = []
|
self.nextUpItems = []
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,22 @@ import JellyfinAPI
|
||||||
|
|
||||||
class ServerDetailViewModel: ViewModel {
|
class ServerDetailViewModel: ViewModel {
|
||||||
|
|
||||||
func refreshServerLibrary() {
|
@Published var server: SwiftfinStore.State.Server
|
||||||
LibraryAPI.refreshLibrary()
|
|
||||||
.trackActivity(loading)
|
init(server: SwiftfinStore.State.Server) {
|
||||||
.sink(receiveCompletion: { completion in
|
self.server = server
|
||||||
self.handleAPIRequestError(completion: completion)
|
}
|
||||||
}, receiveValue: {
|
|
||||||
LogManager.shared.log.debug("Refreshed server library successfully")
|
func setServerCurrentURI(uri: String) {
|
||||||
})
|
SessionManager.main.setServerCurrentURI(server: server, uri: uri)
|
||||||
|
.sink { c in
|
||||||
|
print(c)
|
||||||
|
} receiveValue: { newServerState in
|
||||||
|
self.server = newServerState
|
||||||
|
|
||||||
|
let nc = SwiftfinNotificationCenter.main
|
||||||
|
nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState)
|
||||||
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,20 @@ class UserListViewModel: ViewModel {
|
||||||
|
|
||||||
@Published var users: [SwiftfinStore.State.User] = []
|
@Published var users: [SwiftfinStore.State.User] = []
|
||||||
|
|
||||||
let server: SwiftfinStore.State.Server
|
var server: SwiftfinStore.State.Server
|
||||||
|
|
||||||
init(server: SwiftfinStore.State.Server) {
|
init(server: SwiftfinStore.State.Server) {
|
||||||
self.server = server
|
self.server = server
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
let nc = SwiftfinNotificationCenter.main
|
||||||
|
nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didChangeCurrentLoginURI(_ notification: Notification) {
|
||||||
|
guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") }
|
||||||
|
self.server = newServerState
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUsers() {
|
func fetchUsers() {
|
||||||
|
@ -33,4 +43,5 @@ class UserListViewModel: ViewModel {
|
||||||
SessionManager.main.delete(user: user)
|
SessionManager.main.delete(user: user)
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ final class UserSignInViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(username: String, password: String) {
|
func login(username: String, password: String) {
|
||||||
LogManager.shared.log.debug("Attempting to login to server at \"\(server.uri)\"", tag: "login")
|
LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
|
||||||
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
|
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
|
||||||
|
|
||||||
SessionManager.main.loginUser(server: server, username: username, password: password)
|
SessionManager.main.loginUser(server: server, username: username, password: password)
|
||||||
|
|
Binary file not shown.
|
@ -31,7 +31,7 @@ struct NextUpWidgetProvider: TimelineProvider {
|
||||||
let savedUser = currentLogin.user
|
let savedUser = currentLogin.user
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
var tempCancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
JellyfinAPI.basePath = server.uri
|
JellyfinAPI.basePath = server.currentURI
|
||||||
TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3,
|
TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
||||||
|
@ -76,7 +76,7 @@ struct NextUpWidgetProvider: TimelineProvider {
|
||||||
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
var tempCancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
JellyfinAPI.basePath = server.uri
|
JellyfinAPI.basePath = server.currentURI
|
||||||
TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3,
|
TvShowsAPI.getNextUp(userId: savedUser.id, limit: 3,
|
||||||
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
|
||||||
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
||||||
|
|
Loading…
Reference in New Issue