Replace globalData with SessionManager, ServerEnvironment

This commit is contained in:
PangMo5 2021-06-15 19:15:23 +09:00
parent c66fce752a
commit 8c0c51fa26
17 changed files with 136 additions and 147 deletions

View File

@ -57,8 +57,6 @@
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; };
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; };
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; };
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; };
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
@ -195,7 +193,6 @@
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = "<group>"; };
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = "<group>"; };
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.swift; sourceTree = "<group>"; };
53D5E3DA264B460200BADDC8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = "<group>"; };
@ -398,7 +395,6 @@
children = (
5364F454266CA0DC0026ECBA /* APIExtensions.swift */,
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */,
621338B22660A07800A81A2A /* LazyView.swift */,
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
@ -615,7 +611,6 @@
535870652669D21600D05A09 /* ContentView.swift in Sources */,
62EC353526766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */,
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
@ -654,7 +649,6 @@
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,

View File

@ -79,13 +79,13 @@ struct ContentView: View {
header.append("Token=\"\(globalData.authToken)\"")
globalData.authHeader = header
JellyfinAPI.basePath = globalData.server.baseURI ?? ""
JellyfinAPI.basePath = ServerEnvironment.current.server.baseURI ?? ""
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
DispatchQueue.global(qos: .userInitiated).async {
UserAPI.getCurrentUser()
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
loadState = loadState - 1
}, receiveValue: { response in
libraries = response.configuration?.orderedViews ?? []
@ -100,9 +100,9 @@ struct ContentView: View {
})
.store(in: &globalData.pendingAPIRequests)
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
loadState = loadState - 1
}, receiveValue: { response in
response.items?.forEach({ item in
@ -146,7 +146,7 @@ struct ContentView: View {
.onAppear(perform: startup)
} else {
if !jsi.did {
if isLoading || globalData.user == nil || globalData.user.user_id == nil {
if isLoading || globalData.user == nil || SessionManager.current.userID == nil {
ProgressView()
.onAppear(perform: startup)
} else {

View File

@ -8,6 +8,7 @@
import SwiftUI
import JellyfinAPI
import Combine
struct ProgressBar: Shape {
func path(in rect: CGRect) -> Path {
@ -31,19 +32,19 @@ struct ProgressBar: Shape {
}
struct ContinueWatchingView: View {
@EnvironmentObject var globalData: GlobalData
@State private var items: [BaseItemDto] = []
func onAppear() {
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getResumeItems(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -56,7 +57,7 @@ struct ContinueWatchingView: View {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 320), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 320), bh: item.getBackdropImageBlurHash())
.frame(width: 320, height: 180)
.cornerRadius(10)
.overlay(

View File

@ -7,9 +7,9 @@
import SwiftUI
import JellyfinAPI
import Combine
struct EpisodeItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@EnvironmentObject private var playbackInfo: VideoPlayerItem
@ -18,21 +18,22 @@ struct EpisodeItemView: View {
@State private var settingState: Bool = true
@State private var watched: Bool = false {
didSet {
var tempCancellables = Set<AnyCancellable>()
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
}
@ -41,28 +42,29 @@ struct EpisodeItemView: View {
@State
private var favorite: Bool = false {
didSet {
var tempCancellables = Set<AnyCancellable>()
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -70,7 +72,7 @@ struct EpisodeItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -190,7 +192,7 @@ struct EpisodeItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
@ -229,7 +231,7 @@ struct EpisodeItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -237,7 +239,7 @@ struct EpisodeItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
@ -361,7 +363,7 @@ struct EpisodeItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)

View File

@ -15,7 +15,6 @@ class VideoPlayerItem: ObservableObject {
}
struct ItemView: View {
@EnvironmentObject private var globalData: GlobalData
private var item: BaseItemDto
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()

View File

@ -7,9 +7,9 @@
import SwiftUI
import JellyfinAPI
import Combine
struct LatestMediaView: View {
@EnvironmentObject var globalData: GlobalData
@State var items: [BaseItemDto] = []
private var library_id: String = ""
@ -24,15 +24,17 @@ struct LatestMediaView: View {
return
}
viewDidLoad = true
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.userID!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
items = response
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -45,7 +47,7 @@ struct LatestMediaView: View {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height: 5)

View File

@ -9,7 +9,6 @@ import SwiftUI
import JellyfinAPI
struct LibraryFilterView: View {
@EnvironmentObject var globalData: GlobalData
@Binding var filter: LibraryFilters
var body: some View {

View File

@ -7,9 +7,9 @@
import SwiftUI
import JellyfinAPI
import Combine
struct LibrarySearchView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State private var items: [BaseItemDto] = []
@ -29,16 +29,16 @@ struct LibrarySearchView: View {
func requestSearch(query: String) {
isLoading = true
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
items = response.items ?? []
isLoading = false
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -68,7 +68,7 @@ struct LibrarySearchView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")

View File

@ -9,9 +9,9 @@
import SwiftUI
import NukeUI
import JellyfinAPI
import Combine
struct LibraryView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State private var items: [BaseItemDto] = []
@ -67,11 +67,13 @@ struct LibraryView: View {
isLoading = true
items = []
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
isLoading = false
}, receiveValue: { response in
let x = ceil(Double(response.totalRecordCount!) / 100.0)
@ -80,7 +82,7 @@ struct LibraryView: View {
isLoading = false
viewDidLoad = true
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -107,7 +109,7 @@ struct LibraryView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")

View File

@ -7,9 +7,9 @@
import SwiftUI
import JellyfinAPI
import Combine
struct MovieItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@EnvironmentObject private var playbackInfo: VideoPlayerItem
@ -18,21 +18,23 @@ struct MovieItemView: View {
@State private var settingState: Bool = true
@State private var watched: Bool = false {
didSet {
var tempCancellables = Set<AnyCancellable>()
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
}
@ -41,28 +43,30 @@ struct MovieItemView: View {
@State
private var favorite: Bool = false {
didSet {
var tempCancellables = Set<AnyCancellable>()
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -70,7 +74,7 @@ struct MovieItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120))
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120))
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -192,7 +196,7 @@ struct MovieItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
@ -231,7 +235,7 @@ struct MovieItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -239,7 +243,7 @@ struct MovieItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
@ -365,7 +369,7 @@ struct MovieItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)

View File

@ -6,6 +6,7 @@
*/
import SwiftUI
import Combine
import JellyfinAPI
struct NextUpView: View {
@ -19,14 +20,16 @@ struct NextUpView: View {
}
viewDidLoad = true
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -43,7 +46,7 @@ struct NextUpView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height: 5)

View File

@ -6,10 +6,10 @@
*/
import SwiftUI
import Combine
import JellyfinAPI
struct SeasonItemView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
var item: BaseItemDto = BaseItemDto()
@ -26,17 +26,18 @@ struct SeasonItemView: View {
if viewDidLoad {
return
}
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.userID!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
isLoading = false
}, receiveValue: { response in
viewDidLoad = true
episodes = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -45,7 +46,7 @@ struct SeasonItemView: View {
if isLoading {
EmptyView()
} else {
ImageView(src: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -53,7 +54,7 @@ struct SeasonItemView: View {
var portraitHeaderOverlayView: some View {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -92,7 +93,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
.shadow(radius: 5)
.frame(width: 150, height: 90)
.cornerRadius(10)
@ -151,7 +152,7 @@ struct SeasonItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -160,7 +161,7 @@ struct SeasonItemView: View {
HStack {
VStack(alignment: .leading) {
Spacer().frame(height: 16)
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 4)
@ -185,7 +186,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
.shadow(radius: 5)
.frame(width: 150, height: 90)
.cornerRadius(10)

View File

@ -7,6 +7,7 @@
import SwiftUI
import JellyfinAPI
import Combine
struct SeriesItemView: View {
@EnvironmentObject private var orientationInfo: OrientationInfo
@ -24,17 +25,19 @@ struct SeriesItemView: View {
}
isLoading = true
var tempCancellables = Set<AnyCancellable>()
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
isLoading = false
viewDidLoad = true
seasons = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempCancellables)
}
}
@ -59,7 +62,7 @@ struct SeriesItemView: View {
ForEach(seasons, id: \.id) { season in
NavigationLink(destination: ItemView(item: season)) {
VStack(alignment: .leading) {
ImageView(src: season.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: season.getPrimaryImageBlurHash())
ImageView(src: season.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: season.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)

View File

@ -24,7 +24,7 @@ struct SettingsView: View {
func onAppear() {
let defaults = UserDefaults.standard
username = globalData.user.username!
username = SessionManager.current.user.username!
inNetworkStreamBitrate = defaults.integer(forKey: "InNetworkBandwidth")
outOfNetworkStreamBitrate = defaults.integer(forKey: "OutOfNetworkBandwidth")
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")

View File

@ -9,6 +9,7 @@ import SwiftUI
import MobileVLCKit
import JellyfinAPI
import MediaPlayer
import Combine
struct Subtitle {
var name: String
@ -38,6 +39,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
weak var delegate: PlayerViewControllerDelegate?
var cancellables = Set<AnyCancellable>()
var mediaPlayer = VLCMediaPlayer()
@IBOutlet weak var timeText: UILabel!
@ -280,26 +282,27 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Fetch max bitrate from UserDefaults depending on current connection mode
let defaults = UserDefaults.standard
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
// globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth")
// Build a device profile
let builder = DeviceProfileBuilder()
builder.setMaxBitrate(bitrate: maxBitrate)
let profile = builder.buildProfile()
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.userID!, 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: globalData.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.userID!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { [self] response in
playSessionId = response.playSessionId ?? ""
let mediaSource = response.mediaSources!.first.self!
if mediaSource.transcodingUrl != nil {
// Item is being transcoded by request of server
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
let streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)")
let item = PlaybackItem()
item.videoType = .transcode
item.videoUrl = streamURL!
@ -312,7 +315,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if stream.type == .subtitle {
var deliveryUrl: URL?
if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
} else {
deliveryUrl = nil
}
@ -339,7 +342,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
playbackItem = item
} else {
// Item will be directly played by the client.
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!
let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.authToken)&Tag=\(mediaSource.eTag!)")!
let item = PlaybackItem()
item.videoUrl = streamURL
@ -353,7 +356,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if stream.type == .subtitle {
var deliveryUrl: URL?
if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
} else {
deliveryUrl = nil
}
@ -409,7 +412,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
mediaPlayer.pause()
mediaPlayer.play()
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
@ -514,12 +517,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback progress report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
@ -527,12 +530,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback stop report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
func sendPlayReport() {
@ -541,19 +544,18 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback start report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var globalData: GlobalData
var loadBinding: Binding<Bool>
var pBinding: Binding<Bool>
@ -590,7 +592,6 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
customViewController.manifest = item
customViewController.delegate = context.coordinator
customViewController.globalData = globalData
return customViewController
}

View File

@ -1,27 +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 Foundation
import Combine
import JellyfinAPI
func HandleAPIRequestCompletion(globalData: GlobalData, completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
globalData.expiredCredentials = true
case .error:
globalData.networkError = true
}
}
break
}
}

View File

@ -18,7 +18,11 @@ final class SessionManager {
static let current = SessionManager()
fileprivate(set) var user: SignedInUser!
fileprivate(set) var authHeader: String!
fileprivate(set) var deviceIDString: String
fileprivate(set) var authToken: String!
fileprivate(set) var deviceID: String
var userID: String? {
user.user_id
}
init() {
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SignedInUser")
@ -27,11 +31,11 @@ final class SessionManager {
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
if let deviceID = keychain.get("DeviceIDString") {
self.deviceIDString = deviceID
if let deviceID = keychain.get("DeviceID") {
self.deviceID = deviceID
} else {
self.deviceIDString = UUID().uuidString
keychain.set(deviceIDString, forKey: "DeviceIDString")
self.deviceID = UUID().uuidString
keychain.set(deviceID, forKey: "DeviceID")
}
guard let authToken = keychain.get("AccessToken_\(user?.user_id ?? "")") else {
@ -50,9 +54,10 @@ final class SessionManager {
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(deviceIDString)\", ")
header.append("DeviceId=\"\(deviceID)\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
if let token = authToken {
self.authToken = token
header.append("Token=\"\(token)\"")
}
@ -66,7 +71,7 @@ final class SessionManager {
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
.map { [unowned self] response -> (SignedInUser, String?) in
let user = SignedInUser(context: PersistenceController.shared.container.viewContext)
user.device_uuid = deviceIDString
user.device_uuid = deviceID
user.username = response.user?.name
user.user_id = response.user?.id
return (user, response.accessToken)