swiftlint autocorrect
This commit is contained in:
parent
6307ae4e26
commit
923af3f013
|
@ -12,13 +12,13 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BasicAppSettingsView: View {
|
struct BasicAppSettingsView: View {
|
||||||
|
|
||||||
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
||||||
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
||||||
@State var resetTapped: Bool = false
|
@State var resetTapped: Bool = false
|
||||||
|
|
||||||
@Default(.appAppearance) var appAppearance
|
@Default(.appAppearance) var appAppearance
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -32,7 +32,7 @@ struct BasicAppSettingsView: View {
|
||||||
} header: {
|
} header: {
|
||||||
L10n.accessibility.text
|
L10n.accessibility.text
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
resetTapped = true
|
resetTapped = true
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -10,10 +10,10 @@ import SwiftUI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ConnectToServerViewModel()
|
@StateObject var viewModel = ConnectToServerViewModel()
|
||||||
@State var uri = ""
|
@State var uri = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
|
@ -36,7 +36,7 @@ struct ConnectToServerView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Connect to a Jellyfin server")
|
Text("Connect to a Jellyfin server")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: L10n.localServers.text) {
|
Section(header: L10n.localServers.text) {
|
||||||
if viewModel.searching {
|
if viewModel.searching {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
|
@ -16,7 +16,7 @@ struct ContinueWatchingView: View {
|
||||||
@Namespace private var namespace
|
@Namespace private var namespace
|
||||||
|
|
||||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if items.count > 0 {
|
if items.count > 0 {
|
||||||
|
|
|
@ -39,7 +39,7 @@ struct ItemView: View {
|
||||||
SeasonItemView(viewModel: .init(item: item))
|
SeasonItemView(viewModel: .init(item: item))
|
||||||
} else if item.type == "Episode" {
|
} else if item.type == "Episode" {
|
||||||
EpisodeItemView(viewModel: .init(item: item))
|
EpisodeItemView(viewModel: .init(item: item))
|
||||||
} else {
|
} else {
|
||||||
Text(L10n.notImplementedYetWithType(item.type ?? ""))
|
Text(L10n.notImplementedYetWithType(item.type ?? ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryFilterView: View {
|
struct LibraryFilterView: View {
|
||||||
|
|
||||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||||
@Binding var filters: LibraryFilters
|
@Binding var filters: LibraryFilters
|
||||||
var parentId: String = ""
|
var parentId: String = ""
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct MovieLibrariesView: View {
|
||||||
@EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router
|
@EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router
|
||||||
@StateObject var viewModel: MovieLibrariesViewModel
|
@StateObject var viewModel: MovieLibrariesViewModel
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.isLoading == true {
|
if viewModel.isLoading == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
||||||
|
|
||||||
struct NextUpView: View {
|
struct NextUpView: View {
|
||||||
var items: [BaseItemDto]
|
var items: [BaseItemDto]
|
||||||
|
|
||||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
@ -11,10 +11,10 @@ import CoreStore
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ServerListView: View {
|
struct ServerListView: View {
|
||||||
|
|
||||||
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
||||||
@ObservedObject var viewModel: ServerListViewModel
|
@ObservedObject var viewModel: ServerListViewModel
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listView: some View {
|
private var listView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -27,22 +27,22 @@ struct ServerListView: View {
|
||||||
Image(systemName: "server.rack")
|
Image(systemName: "server.rack")
|
||||||
.font(.system(size: 72))
|
.font(.system(size: 72))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text(server.name)
|
Text(server.name)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Text(server.uri)
|
Text(server.uri)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text(viewModel.userTextFor(server: server))
|
Text(viewModel.userTextFor(server: server))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ struct ServerListView: View {
|
||||||
}
|
}
|
||||||
.padding(.top, 50)
|
.padding(.top, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var noServerView: some View {
|
private var noServerView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -68,7 +68,7 @@ struct ServerListView: View {
|
||||||
.frame(minWidth: 50, maxWidth: 500)
|
.frame(minWidth: 50, maxWidth: 500)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
serverListRouter.route(to: \.connectToServer)
|
serverListRouter.route(to: \.connectToServer)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -79,7 +79,7 @@ struct ServerListView: View {
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
if viewModel.servers.isEmpty {
|
if viewModel.servers.isEmpty {
|
||||||
|
@ -89,7 +89,7 @@ struct ServerListView: View {
|
||||||
listView
|
listView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var trailingToolbarContent: some View {
|
private var trailingToolbarContent: some View {
|
||||||
if viewModel.servers.isEmpty {
|
if viewModel.servers.isEmpty {
|
||||||
|
@ -109,7 +109,7 @@ struct ServerListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
innerBody
|
innerBody
|
||||||
.navigationTitle("Servers")
|
.navigationTitle("Servers")
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct TVLibrariesView: View {
|
||||||
@EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router
|
@EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router
|
||||||
@StateObject var viewModel: TVLibrariesViewModel
|
@StateObject var viewModel: TVLibrariesViewModel
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.isLoading == true {
|
if viewModel.isLoading == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserListView: View {
|
struct UserListView: View {
|
||||||
|
|
||||||
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
||||||
@ObservedObject var viewModel: UserListViewModel
|
@ObservedObject var viewModel: UserListViewModel
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var listView: some View {
|
private var listView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -25,9 +25,9 @@ struct UserListView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Text(user.username)
|
Text(user.username)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
.padding(.top, 50)
|
.padding(.top, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var noUserView: some View {
|
private var noUserView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -55,7 +55,7 @@ struct UserListView: View {
|
||||||
.frame(minWidth: 50, maxWidth: 500)
|
.frame(minWidth: 50, maxWidth: 500)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -66,7 +66,7 @@ struct UserListView: View {
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
if viewModel.users.isEmpty {
|
if viewModel.users.isEmpty {
|
||||||
|
@ -76,7 +76,7 @@ struct UserListView: View {
|
||||||
listView
|
listView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var toolbarContent: some View {
|
private var toolbarContent: some View {
|
||||||
if viewModel.users.isEmpty {
|
if viewModel.users.isEmpty {
|
||||||
|
@ -91,7 +91,7 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
innerBody
|
innerBody
|
||||||
.navigationTitle(viewModel.server.name)
|
.navigationTitle(viewModel.server.name)
|
||||||
|
|
|
@ -11,23 +11,23 @@ import SwiftUI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
struct UserSignInView: View {
|
struct UserSignInView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: UserSignInViewModel
|
@ObservedObject var viewModel: UserSignInViewModel
|
||||||
@State private var username: String = ""
|
@State private var username: String = ""
|
||||||
@State private var password: String = ""
|
@State private var password: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.username, text: $username)
|
TextField(L10n.username, text: $username)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
SecureField(L10n.password, text: $password)
|
SecureField(L10n.password, text: $password)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: username, password: password)
|
viewModel.login(username: username, password: password)
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -138,7 +138,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
let builder = DeviceProfileBuilder()
|
let builder = DeviceProfileBuilder()
|
||||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
|
|
||||||
let currentUser = SessionManager.main.currentLogin.user
|
let currentUser = SessionManager.main.currentLogin.user
|
||||||
|
|
||||||
let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||||
|
|
|
@ -12,12 +12,12 @@ import UIKit
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
static var orientationLock = UIInterfaceOrientationMask.all
|
static var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||||
|
|
||||||
// Lazily initialize datastack
|
// Lazily initialize datastack
|
||||||
let _ = SwiftfinStore.dataStack
|
_ = SwiftfinStore.dataStack
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ import SwiftUI
|
||||||
import MessageUI
|
import MessageUI
|
||||||
|
|
||||||
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
|
|
||||||
public static let shared = EmailHelper()
|
public static let shared = EmailHelper()
|
||||||
|
|
||||||
override private init() { }
|
override private init() { }
|
||||||
|
|
||||||
func sendLogs(logURL: URL) {
|
func sendLogs(logURL: URL) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import SwiftUI
|
||||||
// MARK: JellyfinPlayerApp
|
// MARK: JellyfinPlayerApp
|
||||||
@main
|
@main
|
||||||
struct JellyfinPlayerApp: App {
|
struct JellyfinPlayerApp: App {
|
||||||
|
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
@Default(.appAppearance) var appAppearance
|
@Default(.appAppearance) var appAppearance
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ struct JellyfinPlayerApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupAppearance() {
|
private func setupAppearance() {
|
||||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,8 +77,7 @@ extension AppURLHandler {
|
||||||
// /Users/{UserID}/Items/{ItemID}
|
// /Users/{UserID}/Items/{ItemID}
|
||||||
if url.pathComponents[safe: 2]?.lowercased() == "items",
|
if url.pathComponents[safe: 2]?.lowercased() == "items",
|
||||||
let userID = url.pathComponents[safe: 1],
|
let userID = url.pathComponents[safe: 1],
|
||||||
let itemID = url.pathComponents[safe: 3]
|
let itemID = url.pathComponents[safe: 3] {
|
||||||
{
|
|
||||||
// It would be nice if the ItemViewModel could be initialized to id later.
|
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||||
getItem(userID: userID, itemID: itemID) { item in
|
getItem(userID: userID, itemID: itemID) { item in
|
||||||
guard let item = item else { return }
|
guard let item = item else { return }
|
||||||
|
|
|
@ -11,10 +11,10 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct EpisodeCardVStackView: View {
|
struct EpisodeCardVStackView: View {
|
||||||
|
|
||||||
let items: [BaseItemDto]
|
let items: [BaseItemDto]
|
||||||
let selectedAction: (BaseItemDto) -> Void
|
let selectedAction: (BaseItemDto) -> Void
|
||||||
|
|
||||||
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
@ -30,7 +30,7 @@ struct EpisodeCardVStackView: View {
|
||||||
.padding(.leading, 2)
|
.padding(.leading, 2)
|
||||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||||
.opacity(1)
|
.opacity(1)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
if item.userData?.played ?? false {
|
if item.userData?.played ?? false {
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "circle.fill")
|
||||||
|
@ -42,7 +42,7 @@ struct EpisodeCardVStackView: View {
|
||||||
.opacity(1)
|
.opacity(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
|
@ -50,7 +50,7 @@ struct EpisodeCardVStackView: View {
|
||||||
selectedAction(item)
|
selectedAction(item)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
// MARK: Image
|
// MARK: Image
|
||||||
ImageView(src: item.getPrimaryImage(maxWidth: 150),
|
ImageView(src: item.getPrimaryImage(maxWidth: 150),
|
||||||
bh: item.getPrimaryImageBlurHash(),
|
bh: item.getPrimaryImageBlurHash(),
|
||||||
|
@ -65,37 +65,37 @@ struct EpisodeCardVStackView: View {
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
.overlay(buildCardOverlayView(item: item), alignment: .topTrailing)
|
.overlay(buildCardOverlayView(item: item), alignment: .topTrailing)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
// MARK: Title
|
// MARK: Title
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(item.getEpisodeLocator() ?? "")
|
Text(item.getEpisodeLocator() ?? "")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text(item.getItemRuntime())
|
Text(item.getItemRuntime())
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Overview
|
// MARK: Overview
|
||||||
Text(item.overview ?? "")
|
Text(item.overview ?? "")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.lineLimit(4)
|
.lineLimit(4)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PillHStackView<ItemType: PillStackable>: View {
|
struct PillHStackView<ItemType: PillStackable>: View {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
// let navigationView: (ItemType) -> NavigationView
|
// let navigationView: (ItemType) -> NavigationView
|
||||||
let selectedAction: (ItemType) -> Void
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(title)
|
Text(title)
|
||||||
|
@ -23,7 +23,7 @@ struct PillHStackView<ItemType: PillStackable>: View {
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(items, id: \.title) { item in
|
ForEach(items, id: \.title) { item in
|
||||||
|
|
|
@ -10,13 +10,13 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||||
|
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
let maxWidth: Int
|
let maxWidth: Int
|
||||||
let horizontalAlignment: HorizontalAlignment
|
let horizontalAlignment: HorizontalAlignment
|
||||||
let topBarView: () -> TopBarView
|
let topBarView: () -> TopBarView
|
||||||
let selectedAction: (ItemType) -> Void
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) {
|
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) {
|
||||||
self.items = items
|
self.items = items
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
|
@ -24,18 +24,18 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
||||||
self.topBarView = topBarView
|
self.topBarView = topBarView
|
||||||
self.selectedAction = selectedAction
|
self.selectedAction = selectedAction
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
topBarView()
|
topBarView()
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer().frame(height: 8)
|
Spacer().frame(height: 8)
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
|
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
|
|
||||||
ForEach(items, id: \.title) { item in
|
ForEach(items, id: \.title) { item in
|
||||||
Button {
|
Button {
|
||||||
selectedAction(item)
|
selectedAction(item)
|
||||||
|
@ -47,7 +47,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
||||||
.frame(width: 100, height: CGFloat(maxWidth))
|
.frame(width: 100, height: CGFloat(maxWidth))
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
|
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.fontWeight(.regular)
|
.fontWeight(.regular)
|
||||||
|
@ -55,7 +55,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
if let description = item.description {
|
if let description = item.description {
|
||||||
Text(description)
|
Text(description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import JellyfinAPI
|
||||||
// Not implemented on iOS, but used by a shared Coordinator.
|
// Not implemented on iOS, but used by a shared Coordinator.
|
||||||
struct PortraitItemElement: View {
|
struct PortraitItemElement: View {
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import UIKit
|
||||||
class RefreshHelper {
|
class RefreshHelper {
|
||||||
var refreshControl: UIRefreshControl?
|
var refreshControl: UIRefreshControl?
|
||||||
var refreshAction: (() -> Void)?
|
var refreshAction: (() -> Void)?
|
||||||
|
|
||||||
@objc func didRefresh() {
|
@objc func didRefresh() {
|
||||||
guard let refreshControl = refreshControl else { return }
|
guard let refreshControl = refreshControl else { return }
|
||||||
refreshAction?()
|
refreshAction?()
|
||||||
|
|
|
@ -12,14 +12,14 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BasicAppSettingsView: View {
|
struct BasicAppSettingsView: View {
|
||||||
|
|
||||||
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
||||||
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
||||||
@State var resetTapped: Bool = false
|
@State var resetTapped: Bool = false
|
||||||
|
|
||||||
@Default(.appAppearance) var appAppearance
|
@Default(.appAppearance) var appAppearance
|
||||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
@ -33,7 +33,7 @@ struct BasicAppSettingsView: View {
|
||||||
} header: {
|
} header: {
|
||||||
L10n.accessibility.text
|
L10n.accessibility.text
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker("Default Scheme", selection: $defaultHTTPScheme) {
|
Picker("Default Scheme", selection: $defaultHTTPScheme) {
|
||||||
ForEach(HTTPScheme.allCases, id: \.self) { scheme in
|
ForEach(HTTPScheme.allCases, id: \.self) { scheme in
|
||||||
|
@ -43,7 +43,7 @@ struct BasicAppSettingsView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Networking")
|
Text("Networking")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
resetTapped = true
|
resetTapped = true
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -11,12 +11,12 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
|
||||||
@StateObject var viewModel: ConnectToServerViewModel
|
@StateObject var viewModel: ConnectToServerViewModel
|
||||||
@State var uri = ""
|
@State var uri = ""
|
||||||
|
|
||||||
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
@Default(.defaultHTTPScheme) var defaultHTTPScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
|
@ -29,7 +29,7 @@ struct ConnectToServerView: View {
|
||||||
uri = "\(defaultHTTPScheme.rawValue)://"
|
uri = "\(defaultHTTPScheme.rawValue)://"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
viewModel.cancelConnection()
|
viewModel.cancelConnection()
|
||||||
|
@ -47,7 +47,7 @@ struct ConnectToServerView: View {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Connect to a Jellyfin server")
|
Text("Connect to a Jellyfin server")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if viewModel.searching {
|
if viewModel.searching {
|
||||||
HStack(alignment: .center, spacing: 5) {
|
HStack(alignment: .center, spacing: 5) {
|
||||||
|
@ -90,7 +90,7 @@ struct ConnectToServerView: View {
|
||||||
HStack {
|
HStack {
|
||||||
L10n.localServers.text
|
L10n.localServers.text
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.discoverServers()
|
viewModel.discoverServers()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -12,10 +12,10 @@ import Introspect
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
|
|
||||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
@StateObject var viewModel = HomeViewModel()
|
@StateObject var viewModel = HomeViewModel()
|
||||||
|
|
||||||
private let refreshHelper = RefreshHelper()
|
private let refreshHelper = RefreshHelper()
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
@ -31,7 +31,7 @@ struct HomeView: View {
|
||||||
if !viewModel.nextUpItems.isEmpty {
|
if !viewModel.nextUpItems.isEmpty {
|
||||||
NextUpView(items: viewModel.nextUpItems)
|
NextUpView(items: viewModel.nextUpItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(viewModel.libraries, id: \.self) { library in
|
ForEach(viewModel.libraries, id: \.self) { library in
|
||||||
HStack {
|
HStack {
|
||||||
Text(L10n.latestWithString(library.name ?? ""))
|
Text(L10n.latestWithString(library.name ?? ""))
|
||||||
|
@ -58,10 +58,10 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
.introspectScrollView { scrollView in
|
.introspectScrollView { scrollView in
|
||||||
let control = UIRefreshControl()
|
let control = UIRefreshControl()
|
||||||
|
|
||||||
refreshHelper.refreshControl = control
|
refreshHelper.refreshControl = control
|
||||||
refreshHelper.refreshAction = viewModel.refresh
|
refreshHelper.refreshAction = viewModel.refresh
|
||||||
|
|
||||||
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
|
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
|
||||||
scrollView.refreshControl = control
|
scrollView.refreshControl = control
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ struct ItemNavigationView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct ItemView: View {
|
private struct ItemView: View {
|
||||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
|
|
||||||
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
|
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
|
||||||
|
|
|
@ -10,22 +10,22 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ItemLandscapeTopBarView: View {
|
struct ItemLandscapeTopBarView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
// MARK: Name
|
// MARK: Name
|
||||||
|
|
||||||
Text(viewModel.getItemDisplayName())
|
Text(viewModel.getItemDisplayName())
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if viewModel.item.itemType.showDetails {
|
if viewModel.item.itemType.showDetails {
|
||||||
// MARK: Runtime
|
// MARK: Runtime
|
||||||
Text(viewModel.item.getItemRuntime())
|
Text(viewModel.item.getItemRuntime())
|
||||||
|
@ -34,7 +34,7 @@ struct ItemLandscapeTopBarView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Details
|
// MARK: Details
|
||||||
HStack {
|
HStack {
|
||||||
if viewModel.item.productionYear != nil {
|
if viewModel.item.productionYear != nil {
|
||||||
|
@ -53,9 +53,9 @@ struct ItemLandscapeTopBarView: View {
|
||||||
.overlay(RoundedRectangle(cornerRadius: 2)
|
.overlay(RoundedRectangle(cornerRadius: 2)
|
||||||
.stroke(Color.secondary, lineWidth: 1))
|
.stroke(Color.secondary, lineWidth: 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if viewModel.item.itemType.showDetails {
|
if viewModel.item.itemType.showDetails {
|
||||||
// MARK: Favorite
|
// MARK: Favorite
|
||||||
Button {
|
Button {
|
||||||
|
@ -70,7 +70,7 @@ struct ItemLandscapeTopBarView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(viewModel.isLoading)
|
.disabled(viewModel.isLoading)
|
||||||
|
|
||||||
// MARK: Watched
|
// MARK: Watched
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateWatchState()
|
viewModel.updateWatchState()
|
||||||
|
|
|
@ -11,22 +11,22 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct PortraitHeaderOverlayView: View {
|
struct PortraitHeaderOverlayView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .bottom, spacing: 12) {
|
HStack(alignment: .bottom, spacing: 12) {
|
||||||
|
|
||||||
// MARK: Portrait Image
|
// MARK: Portrait Image
|
||||||
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130))
|
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130))
|
||||||
.frame(width: 130, height: 195)
|
.frame(width: 130, height: 195)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// MARK: Name
|
// MARK: Name
|
||||||
Text(viewModel.getItemDisplayName())
|
Text(viewModel.getItemDisplayName())
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
|
@ -34,7 +34,7 @@ struct PortraitHeaderOverlayView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if viewModel.item.itemType.showDetails {
|
if viewModel.item.itemType.showDetails {
|
||||||
// MARK: Runtime
|
// MARK: Runtime
|
||||||
if viewModel.shouldDisplayRuntime() {
|
if viewModel.shouldDisplayRuntime() {
|
||||||
|
@ -45,7 +45,7 @@ struct PortraitHeaderOverlayView: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Details
|
// MARK: Details
|
||||||
HStack {
|
HStack {
|
||||||
if let productionYear = viewModel.item.productionYear {
|
if let productionYear = viewModel.item.productionYear {
|
||||||
|
@ -55,7 +55,7 @@ struct PortraitHeaderOverlayView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let officialRating = viewModel.item.officialRating {
|
if let officialRating = viewModel.item.officialRating {
|
||||||
Text(officialRating)
|
Text(officialRating)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
@ -70,9 +70,9 @@ struct PortraitHeaderOverlayView: View {
|
||||||
}
|
}
|
||||||
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30)
|
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
// MARK: Play
|
// MARK: Play
|
||||||
Button {
|
Button {
|
||||||
if let playButtonItem = viewModel.playButtonItem {
|
if let playButtonItem = viewModel.playButtonItem {
|
||||||
|
@ -93,9 +93,9 @@ struct PortraitHeaderOverlayView: View {
|
||||||
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}.disabled(viewModel.playButtonItem == nil)
|
}.disabled(viewModel.playButtonItem == nil)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if viewModel.item.itemType.showDetails {
|
if viewModel.item.itemType.showDetails {
|
||||||
// MARK: Favorite
|
// MARK: Favorite
|
||||||
Button {
|
Button {
|
||||||
|
@ -112,7 +112,7 @@ struct PortraitHeaderOverlayView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(viewModel.isLoading)
|
.disabled(viewModel.isLoading)
|
||||||
|
|
||||||
// MARK: Watched
|
// MARK: Watched
|
||||||
Button {
|
Button {
|
||||||
viewModel.updateWatchState()
|
viewModel.updateWatchState()
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryFilterView: View {
|
struct LibraryFilterView: View {
|
||||||
|
|
||||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||||
@Binding var filters: LibraryFilters
|
@Binding var filters: LibraryFilters
|
||||||
var parentId: String = ""
|
var parentId: String = ""
|
||||||
|
|
|
@ -11,10 +11,10 @@ import CoreStore
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ServerListView: View {
|
struct ServerListView: View {
|
||||||
|
|
||||||
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
||||||
@ObservedObject var viewModel: ServerListViewModel
|
@ObservedObject var viewModel: ServerListViewModel
|
||||||
|
|
||||||
private var listView: some View {
|
private var listView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
|
@ -27,22 +27,22 @@ struct ServerListView: View {
|
||||||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Image(systemName: "server.rack")
|
Image(systemName: "server.rack")
|
||||||
.font(.system(size: 36))
|
.font(.system(size: 36))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text(server.name)
|
Text(server.name)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
Text(server.uri)
|
Text(server.uri)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text(viewModel.userTextFor(server: server))
|
Text(viewModel.userTextFor(server: server))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
@ -62,13 +62,13 @@ struct ServerListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noServerView: some View {
|
private var noServerView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Connect to a Jellyfin server to get started")
|
Text("Connect to a Jellyfin server to get started")
|
||||||
.frame(minWidth: 50, maxWidth: 240)
|
.frame(minWidth: 50, maxWidth: 240)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
serverListRouter.route(to: \.connectToServer)
|
serverListRouter.route(to: \.connectToServer)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -80,7 +80,7 @@ struct ServerListView: View {
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.padding([.top, .bottom], 20)
|
.padding([.top, .bottom], 20)
|
||||||
|
|
||||||
L10n.connect.text
|
L10n.connect.text
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
.bold()
|
.bold()
|
||||||
|
@ -88,7 +88,7 @@ struct ServerListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
if viewModel.servers.isEmpty {
|
if viewModel.servers.isEmpty {
|
||||||
|
@ -98,7 +98,7 @@ struct ServerListView: View {
|
||||||
listView
|
listView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var trailingToolbarContent: some View {
|
private var trailingToolbarContent: some View {
|
||||||
if viewModel.servers.isEmpty {
|
if viewModel.servers.isEmpty {
|
||||||
|
@ -111,7 +111,7 @@ struct ServerListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var leadingToolbarContent: some View {
|
private var leadingToolbarContent: some View {
|
||||||
Button {
|
Button {
|
||||||
serverListRouter.route(to: \.basicAppSettings)
|
serverListRouter.route(to: \.basicAppSettings)
|
||||||
|
@ -119,7 +119,7 @@ struct ServerListView: View {
|
||||||
Image(systemName: "gearshape.fill")
|
Image(systemName: "gearshape.fill")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
innerBody
|
innerBody
|
||||||
.navigationTitle("Servers")
|
.navigationTitle("Servers")
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||||
@ObservedObject var viewModel: SettingsViewModel
|
@ObservedObject var viewModel: SettingsViewModel
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ struct SettingsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section(header: EmptyView()) {
|
Section(header: EmptyView()) {
|
||||||
|
|
||||||
// There is a bug where the SettingsView attmempts to remake itself upon signing out
|
// There is a bug where the SettingsView attmempts to remake itself upon signing out
|
||||||
// so this check is made
|
// so this check is made
|
||||||
if SessionManager.main.currentLogin == nil {
|
if SessionManager.main.currentLogin == nil {
|
||||||
|
@ -81,7 +81,7 @@ struct SettingsView: View {
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Playback")) {
|
Section(header: Text("Playback")) {
|
||||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserListView: View {
|
struct UserListView: View {
|
||||||
|
|
||||||
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
||||||
@ObservedObject var viewModel: UserListViewModel
|
@ObservedObject var viewModel: UserListViewModel
|
||||||
|
|
||||||
private var listView: some View {
|
private var listView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
|
@ -26,13 +26,13 @@ struct UserListView: View {
|
||||||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(user.username)
|
Text(user.username)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
@ -51,13 +51,13 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noUserView: some View {
|
private var noUserView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Sign in to get started")
|
Text("Sign in to get started")
|
||||||
.frame(minWidth: 50, maxWidth: 240)
|
.frame(minWidth: 50, maxWidth: 240)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -69,7 +69,7 @@ struct UserListView: View {
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.padding(.horizontal, 30)
|
.padding(.horizontal, 30)
|
||||||
.padding([.top, .bottom], 20)
|
.padding([.top, .bottom], 20)
|
||||||
|
|
||||||
Text("Sign in")
|
Text("Sign in")
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
.bold()
|
.bold()
|
||||||
|
@ -77,7 +77,7 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
if viewModel.users.isEmpty {
|
if viewModel.users.isEmpty {
|
||||||
|
@ -87,7 +87,7 @@ struct UserListView: View {
|
||||||
listView
|
listView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var toolbarContent: some View {
|
private var toolbarContent: some View {
|
||||||
if viewModel.users.isEmpty {
|
if viewModel.users.isEmpty {
|
||||||
|
@ -102,7 +102,7 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
innerBody
|
innerBody
|
||||||
.navigationTitle(viewModel.server.name)
|
.navigationTitle(viewModel.server.name)
|
||||||
|
|
|
@ -11,23 +11,23 @@ import SwiftUI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
struct UserSignInView: View {
|
struct UserSignInView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: UserSignInViewModel
|
@ObservedObject var viewModel: UserSignInViewModel
|
||||||
@State private var username: String = ""
|
@State private var username: String = ""
|
||||||
@State private var password: String = ""
|
@State private var password: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.username, text: $username)
|
TextField(L10n.username, text: $username)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
SecureField(L10n.password, text: $password)
|
SecureField(L10n.password, text: $password)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
viewModel.cancelSignIn()
|
viewModel.cancelSignIn()
|
||||||
|
|
|
@ -153,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
} else {
|
} else {
|
||||||
sendJellyfinCommand(command: "Seek", options: [
|
sendJellyfinCommand(command: "Seek", options: [
|
||||||
"position": Int(secondsScrubbedTo),
|
"position": Int(secondsScrubbedTo)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -664,8 +664,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
subtitleTrackArray.forEach { subtitle in
|
subtitleTrackArray.forEach { subtitle in
|
||||||
if Defaults[.isAutoSelectSubtitles] {
|
if Defaults[.isAutoSelectSubtitles] {
|
||||||
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
||||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "")
|
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
|
||||||
{
|
|
||||||
selectedCaptionTrack = subtitle.id
|
selectedCaptionTrack = subtitle.id
|
||||||
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||||
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
||||||
|
@ -854,7 +853,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
if hasSentRemoteSeek == false {
|
if hasSentRemoteSeek == false {
|
||||||
hasSentRemoteSeek = true
|
hasSentRemoteSeek = true
|
||||||
sendJellyfinCommand(command: "Seek", options: [
|
sendJellyfinCommand(command: "Seek", options: [
|
||||||
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position),
|
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -880,7 +879,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
"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!,
|
||||||
"subtitleBurnIn": false,
|
"subtitleBurnIn": false
|
||||||
]
|
]
|
||||||
let jsonData = JSON(payload)
|
let jsonData = JSON(payload)
|
||||||
|
|
||||||
|
@ -935,8 +934,8 @@ extension PlayerViewController: GCKSessionManagerListener {
|
||||||
"Name": manifest.name!,
|
"Name": manifest.name!,
|
||||||
"Type": manifest.type!,
|
"Type": manifest.type!,
|
||||||
"MediaType": manifest.mediaType!,
|
"MediaType": manifest.mediaType!,
|
||||||
"IsFolder": manifest.isFolder!,
|
"IsFolder": manifest.isFolder!
|
||||||
]],
|
]]
|
||||||
]
|
]
|
||||||
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
||||||
}
|
}
|
||||||
|
@ -1104,8 +1103,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
|
|
||||||
typealias UIViewControllerType = PlayerViewController
|
typealias UIViewControllerType = PlayerViewController
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls
|
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls
|
||||||
.UIViewControllerType
|
.UIViewControllerType {
|
||||||
{
|
|
||||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||||
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||||
customViewController.manifest = item
|
customViewController.manifest = item
|
||||||
|
|
|
@ -12,11 +12,11 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class BasicAppSettingsCoordinator: NavigationCoordinatable {
|
final class BasicAppSettingsCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start)
|
let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
|
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,16 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class ConnectToServerCoodinator: NavigationCoordinatable {
|
final class ConnectToServerCoodinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
|
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
@Route(.push) var userSignIn = makeUserSignIn
|
@Route(.push) var userSignIn = makeUserSignIn
|
||||||
|
|
||||||
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
||||||
return UserSignInCoordinator(viewModel: .init(server: server))
|
return UserSignInCoordinator(viewModel: .init(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
ConnectToServerView(viewModel: ConnectToServerViewModel())
|
ConnectToServerView(viewModel: ConnectToServerViewModel())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,9 @@ import SwiftUI
|
||||||
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||||
|
|
||||||
final class FilterCoordinator: NavigationCoordinatable {
|
final class FilterCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
||||||
@Binding var filters: LibraryFilters
|
@Binding var filters: LibraryFilters
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class HomeCoordinator: NavigationCoordinatable {
|
final class HomeCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
@ -34,11 +34,11 @@ final class HomeCoordinator: NavigationCoordinatable {
|
||||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
ItemCoordinator(item: item)
|
ItemCoordinator(item: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||||
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
|
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
|
||||||
return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
|
return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class ItemCoordinator: NavigationCoordinatable {
|
final class ItemCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \ItemCoordinator.start)
|
let stack = NavigationStack(initial: \ItemCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -15,7 +15,7 @@ import SwiftUI
|
||||||
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
||||||
|
|
||||||
final class LibraryCoordinator: NavigationCoordinatable {
|
final class LibraryCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
@ -49,7 +49,7 @@ final class LibraryCoordinator: NavigationCoordinatable {
|
||||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
ItemCoordinator(item: item)
|
ItemCoordinator(item: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||||
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class LibraryListCoordinator: NavigationCoordinatable {
|
final class LibraryListCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -25,13 +25,13 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
} else {
|
} else {
|
||||||
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||||
|
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
||||||
|
|
||||||
// Back bar button item setup
|
// Back bar button item setup
|
||||||
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
||||||
let barAppearance = UINavigationBar.appearance()
|
let barAppearance = UINavigationBar.appearance()
|
||||||
|
|
|
@ -42,7 +42,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
view.onAppear {
|
view.onAppear {
|
||||||
AppURLHandler.shared.appURLState = .allowed
|
AppURLHandler.shared.appURLState = .allowed
|
||||||
// TODO: todo
|
// TODO: todo
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||||
AppURLHandler.shared.processLaunchedURLIfNeeded()
|
AppURLHandler.shared.processLaunchedURLIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,14 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@Root var mainTab = makeMainTab
|
@Root var mainTab = makeMainTab
|
||||||
@Root var serverList = makeServerList
|
@Root var serverList = makeServerList
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if SessionManager.main.currentLogin != nil {
|
if SessionManager.main.currentLogin != nil {
|
||||||
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
|
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
|
||||||
} else {
|
} else {
|
||||||
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||||
|
|
||||||
|
|
|
@ -19,24 +19,24 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
\MainTabCoordinator.other,
|
\MainTabCoordinator.other,
|
||||||
\MainTabCoordinator.settings
|
\MainTabCoordinator.settings
|
||||||
])
|
])
|
||||||
|
|
||||||
@Route(tabItem: makeHomeTab) var home = makeHome
|
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||||
@Route(tabItem: makeTvTab) var tv = makeTv
|
@Route(tabItem: makeTvTab) var tv = makeTv
|
||||||
@Route(tabItem: makeMoviesTab) var movies = makeMovies
|
@Route(tabItem: makeMoviesTab) var movies = makeMovies
|
||||||
@Route(tabItem: makeOtherTab) var other = makeOther
|
@Route(tabItem: makeOtherTab) var other = makeOther
|
||||||
@Route(tabItem: makeSettingsTab) var settings = makeSettings
|
@Route(tabItem: makeSettingsTab) var settings = makeSettings
|
||||||
|
|
||||||
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
||||||
return NavigationViewCoordinator(HomeCoordinator())
|
return NavigationViewCoordinator(HomeCoordinator())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "house")
|
Image(systemName: "house")
|
||||||
L10n.home.text
|
L10n.home.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
|
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
|
||||||
return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
|
return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
Text("TV Shows")
|
Text("TV Shows")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
|
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
|
||||||
return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
|
return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
|
||||||
}
|
}
|
||||||
|
@ -69,11 +69,11 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
Text("Other")
|
Text("Other")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
||||||
return NavigationViewCoordinator(SettingsCoordinator())
|
return NavigationViewCoordinator(SettingsCoordinator())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View {
|
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "gearshape.fill")
|
Image(systemName: "gearshape.fill")
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
|
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -13,7 +13,7 @@ import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
final class SearchCoordinator: NavigationCoordinatable {
|
final class SearchCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \SearchCoordinator.start)
|
let stack = NavigationStack(initial: \SearchCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -12,26 +12,26 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class ServerListCoordinator: NavigationCoordinatable {
|
final class ServerListCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \ServerListCoordinator.start)
|
let stack = NavigationStack(initial: \ServerListCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
@Route(.push) var connectToServer = makeConnectToServer
|
@Route(.push) var connectToServer = makeConnectToServer
|
||||||
@Route(.push) var userList = makeUserList
|
@Route(.push) var userList = makeUserList
|
||||||
@Route(.modal) var basicAppSettings = makeBasicAppSettings
|
@Route(.modal) var basicAppSettings = makeBasicAppSettings
|
||||||
|
|
||||||
func makeConnectToServer() -> ConnectToServerCoodinator {
|
func makeConnectToServer() -> ConnectToServerCoodinator {
|
||||||
ConnectToServerCoodinator()
|
ConnectToServerCoodinator()
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
|
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
|
||||||
UserListCoordinator(viewModel: .init(server: server))
|
UserListCoordinator(viewModel: .init(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
|
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
|
||||||
NavigationViewCoordinator(BasicAppSettingsCoordinator())
|
NavigationViewCoordinator(BasicAppSettingsCoordinator())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
ServerListView(viewModel: ServerListViewModel())
|
ServerListView(viewModel: ServerListViewModel())
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class SettingsCoordinator: NavigationCoordinatable {
|
final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class TVLibrariesCoordinator: NavigationCoordinatable {
|
final class TVLibrariesCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
|
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
|
@ -12,22 +12,22 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class UserListCoordinator: NavigationCoordinatable {
|
final class UserListCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \UserListCoordinator.start)
|
let stack = NavigationStack(initial: \UserListCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
@Route(.push) var userSignIn = makeUserSignIn
|
@Route(.push) var userSignIn = makeUserSignIn
|
||||||
|
|
||||||
let viewModel: UserListViewModel
|
let viewModel: UserListViewModel
|
||||||
|
|
||||||
init(viewModel: UserListViewModel) {
|
init(viewModel: UserListViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
|
||||||
return UserSignInCoordinator(viewModel: .init(server: server))
|
return UserSignInCoordinator(viewModel: .init(server: server))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
UserListView(viewModel: viewModel)
|
UserListView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,17 +12,17 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class UserSignInCoordinator: NavigationCoordinatable {
|
final class UserSignInCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \UserSignInCoordinator.start)
|
let stack = NavigationStack(initial: \UserSignInCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
||||||
let viewModel: UserSignInViewModel
|
let viewModel: UserSignInViewModel
|
||||||
|
|
||||||
init(viewModel: UserSignInViewModel) {
|
init(viewModel: UserSignInViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
UserSignInView(viewModel: viewModel)
|
UserSignInView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,11 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class VideoPlayerCoordinator: NavigationCoordinatable {
|
final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
|
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
|
||||||
|
|
||||||
@Root var start = makeStart
|
@Root var start = makeStart
|
||||||
|
|
||||||
let item: BaseItemDto
|
let item: BaseItemDto
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ struct ErrorMessage: Identifiable {
|
||||||
let title: String
|
let title: String
|
||||||
let displayMessage: String
|
let displayMessage: String
|
||||||
let logConstructor: LogConstructor
|
let logConstructor: LogConstructor
|
||||||
|
|
||||||
// Chosen value such that if an error has this code, don't show the code to the UI
|
// Chosen value such that if an error has this code, don't show the code to the UI
|
||||||
// This was chosen because of its unlikelyhood to ever be used
|
// This was chosen because of its unlikelyhood to ever be used
|
||||||
static let noShowErrorCode = -69420
|
static let noShowErrorCode = -69420
|
||||||
|
|
|
@ -15,11 +15,11 @@ extension BaseItemDto: PortraitImageStackable {
|
||||||
public func imageURLContsructor(maxWidth: Int) -> URL {
|
public func imageURLContsructor(maxWidth: Int) -> URL {
|
||||||
return self.getPrimaryImage(maxWidth: maxWidth)
|
return self.getPrimaryImage(maxWidth: maxWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: String {
|
public var title: String {
|
||||||
return self.name ?? ""
|
return self.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
public var description: String? {
|
public var description: String? {
|
||||||
switch self.itemType {
|
switch self.itemType {
|
||||||
case .season:
|
case .season:
|
||||||
|
@ -31,11 +31,11 @@ extension BaseItemDto: PortraitImageStackable {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var blurHash: String {
|
public var blurHash: String {
|
||||||
return self.getPrimaryImageBlurHash()
|
return self.getPrimaryImageBlurHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var failureInitials: String {
|
public var failureInitials: String {
|
||||||
guard let name = self.name else { return "" }
|
guard let name = self.name else { return "" }
|
||||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||||
|
|
|
@ -14,7 +14,7 @@ import UIKit
|
||||||
// 001fC^ = dark grey plain blurhash
|
// 001fC^ = dark grey plain blurhash
|
||||||
|
|
||||||
public extension BaseItemDto {
|
public extension BaseItemDto {
|
||||||
|
|
||||||
// MARK: Images
|
// MARK: Images
|
||||||
|
|
||||||
func getSeriesBackdropImageBlurHash() -> String {
|
func getSeriesBackdropImageBlurHash() -> String {
|
||||||
|
@ -152,17 +152,17 @@ public extension BaseItemDto {
|
||||||
return "\(String(progminutes))m"
|
return "\(String(progminutes))m"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: ItemType
|
// MARK: ItemType
|
||||||
|
|
||||||
enum ItemType: String {
|
enum ItemType: String {
|
||||||
case movie = "Movie"
|
case movie = "Movie"
|
||||||
case season = "Season"
|
case season = "Season"
|
||||||
case episode = "Episode"
|
case episode = "Episode"
|
||||||
case series = "Series"
|
case series = "Series"
|
||||||
|
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
var showDetails: Bool {
|
var showDetails: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .season, .series:
|
case .season, .series:
|
||||||
|
@ -172,14 +172,14 @@ public extension BaseItemDto {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemType: ItemType {
|
var itemType: ItemType {
|
||||||
guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
|
guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
|
||||||
return knownType
|
return knownType
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: PortraitHeaderViewURL
|
// MARK: PortraitHeaderViewURL
|
||||||
|
|
||||||
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
||||||
switch self.itemType {
|
switch self.itemType {
|
||||||
case .movie, .season, .series:
|
case .movie, .season, .series:
|
||||||
|
|
|
@ -10,7 +10,7 @@ import JellyfinAPI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension BaseItemPerson {
|
extension BaseItemPerson {
|
||||||
|
|
||||||
// MARK: Get Image
|
// MARK: Get Image
|
||||||
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let imageType = "Primary"
|
let imageType = "Primary"
|
||||||
|
@ -28,10 +28,9 @@ extension BaseItemPerson {
|
||||||
|
|
||||||
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: First Role
|
// MARK: First Role
|
||||||
|
|
||||||
// Jellyfin will grab all roles the person played in the show which makes the role
|
// Jellyfin will grab all roles the person played in the show which makes the role
|
||||||
// text too long. This will grab the first role which:
|
// text too long. This will grab the first role which:
|
||||||
// - assumes that the most important role is the first
|
// - assumes that the most important role is the first
|
||||||
|
@ -40,16 +39,16 @@ extension BaseItemPerson {
|
||||||
guard let role = self.role else { return nil }
|
guard let role = self.role else { return nil }
|
||||||
let split = role.split(separator: "/")
|
let split = role.split(separator: "/")
|
||||||
guard split.count > 1 else { return role }
|
guard split.count > 1 else { return role }
|
||||||
|
|
||||||
guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")), let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role }
|
guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")), let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role }
|
||||||
|
|
||||||
var final = firstRole
|
var final = firstRole
|
||||||
|
|
||||||
if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") {
|
if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") {
|
||||||
let roleText = lastRole[lastOpenIndex...lastClosingIndex]
|
let roleText = lastRole[lastOpenIndex...lastClosingIndex]
|
||||||
final.append(" \(roleText)")
|
final.append(" \(roleText)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return final
|
return final
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,19 +58,19 @@ 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.uri, maxWidth: maxWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: String {
|
public var title: String {
|
||||||
return self.name ?? ""
|
return self.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
public var description: String? {
|
public var description: String? {
|
||||||
return self.firstRole()
|
return self.firstRole()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var blurHash: String {
|
public var blurHash: String {
|
||||||
return self.getBlurHash()
|
return self.getBlurHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var failureInitials: String {
|
public var failureInitials: String {
|
||||||
guard let name = self.name else { return "" }
|
guard let name = self.name else { return "" }
|
||||||
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
let initials = name.split(separator: " ").compactMap({ String($0).first })
|
||||||
|
@ -81,7 +80,7 @@ extension BaseItemPerson: PortraitImageStackable {
|
||||||
|
|
||||||
// MARK: DiplayedType
|
// MARK: DiplayedType
|
||||||
extension BaseItemPerson {
|
extension BaseItemPerson {
|
||||||
|
|
||||||
// Only displayed person types.
|
// Only displayed person types.
|
||||||
// Will ignore people like "GuestStar"
|
// Will ignore people like "GuestStar"
|
||||||
enum DisplayedType: String, CaseIterable {
|
enum DisplayedType: String, CaseIterable {
|
||||||
|
@ -89,7 +88,7 @@ extension BaseItemPerson {
|
||||||
case director = "Director"
|
case director = "Director"
|
||||||
case writer = "Writer"
|
case writer = "Writer"
|
||||||
case producer = "Producer"
|
case producer = "Producer"
|
||||||
|
|
||||||
static var allCasesRaw: [String] {
|
static var allCasesRaw: [String] {
|
||||||
return self.allCases.map({ $0.rawValue })
|
return self.allCases.map({ $0.rawValue })
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,13 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct JellyfinAPIError: Error {
|
struct JellyfinAPIError: Error {
|
||||||
|
|
||||||
private let message: String
|
private let message: String
|
||||||
|
|
||||||
init(_ message: String) {
|
init(_ message: String) {
|
||||||
self.message = message
|
self.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
var localizedDescription: String {
|
var localizedDescription: String {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ extension String {
|
||||||
|
|
||||||
return "\(padString)\(self)"
|
return "\(padString)\(self)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var text: Text {
|
var text: Text {
|
||||||
Text(self)
|
Text(self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,8 @@ enum DetailItemType: String {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DetailItem {
|
struct DetailItem {
|
||||||
|
|
||||||
let baseItem: BaseItemDto
|
let baseItem: BaseItemDto
|
||||||
let type: DetailItemType
|
let type: DetailItemType
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ enum ItemType: String {
|
||||||
case movie = "Movie"
|
case movie = "Movie"
|
||||||
case series = "Series"
|
case series = "Series"
|
||||||
case season = "Season"
|
case season = "Season"
|
||||||
|
|
||||||
var localized: String {
|
var localized: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .episode:
|
case .episode:
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class ServerDiscovery {
|
||||||
case name = "Name"
|
case name = "Name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let broadcastConn: UDPBroadcastConnection
|
private let broadcastConn: UDPBroadcastConnection
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
|
|
@ -19,83 +19,83 @@ typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStor
|
||||||
|
|
||||||
// MARK: NewSessionManager
|
// MARK: NewSessionManager
|
||||||
final class SessionManager {
|
final class SessionManager {
|
||||||
|
|
||||||
// MARK: currentLogin
|
// MARK: currentLogin
|
||||||
private(set) var currentLogin: CurrentLogin!
|
private(set) var currentLogin: CurrentLogin!
|
||||||
|
|
||||||
// MARK: main
|
// MARK: main
|
||||||
static let main = SessionManager()
|
static let main = SessionManager()
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID],
|
if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID],
|
||||||
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) {
|
||||||
|
|
||||||
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.uri
|
||||||
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 {
|
private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String {
|
||||||
return "\(server.id)-\(user.id)"
|
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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
|
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
|
||||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
|
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
else { fatalError("No stored server associated with given state server?") }
|
||||||
return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username })
|
return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connects to a server at the given uri, storing if successful
|
// Connects to a server at the given uri, storing if successful
|
||||||
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
|
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
|
||||||
var uriComponents = URLComponents(string: uri) ?? URLComponents()
|
var uriComponents = URLComponents(string: uri) ?? URLComponents()
|
||||||
|
|
||||||
if uriComponents.scheme == nil {
|
if uriComponents.scheme == nil {
|
||||||
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
|
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var uri = uriComponents.string ?? ""
|
var uri = uriComponents.string ?? ""
|
||||||
|
|
||||||
if uri.last == "/" {
|
if uri.last == "/" {
|
||||||
uri = String(uri.dropLast())
|
uri = String(uri.dropLast())
|
||||||
}
|
}
|
||||||
|
|
||||||
JellyfinAPI.basePath = uri
|
JellyfinAPI.basePath = uri
|
||||||
|
|
||||||
return SystemAPI.getPublicSystemInfo()
|
return SystemAPI.getPublicSystemInfo()
|
||||||
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
|
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>())
|
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>())
|
||||||
|
|
||||||
guard let name = response.serverName,
|
guard let name = response.serverName,
|
||||||
let id = response.id,
|
let id = response.id,
|
||||||
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.uri = uri
|
||||||
newServer.name = name
|
newServer.name = name
|
||||||
newServer.id = id
|
newServer.id = id
|
||||||
newServer.os = os
|
newServer.os = os
|
||||||
newServer.version = version
|
newServer.version = version
|
||||||
newServer.users = []
|
newServer.users = []
|
||||||
|
|
||||||
// Check for existing server on device
|
// Check for existing server on device
|
||||||
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", newServer.id)]) {
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", newServer.id)]) {
|
||||||
throw SwiftfinStore.Errors.existingServer(existingServer.state)
|
throw SwiftfinStore.Errors.existingServer(existingServer.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (newServer, transaction)
|
return (newServer, transaction)
|
||||||
})
|
})
|
||||||
.handleEvents(receiveOutput: { (_, transaction) in
|
.handleEvents(receiveOutput: { (_, transaction) in
|
||||||
|
@ -106,57 +106,57 @@ final class SessionManager {
|
||||||
})
|
})
|
||||||
.eraseToAnyPublisher()
|
.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.uri
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
||||||
|
|
||||||
guard let username = response.user?.name,
|
guard let username = response.user?.name,
|
||||||
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
||||||
|
|
||||||
newUser.username = username
|
newUser.username = username
|
||||||
newUser.id = id
|
newUser.id = id
|
||||||
newUser.appleTVID = ""
|
newUser.appleTVID = ""
|
||||||
|
|
||||||
// Check for existing user on device
|
// Check for existing user on device
|
||||||
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", newUser.id)]) {
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", newUser.id)]) {
|
||||||
throw SwiftfinStore.Errors.existingUser(existingUser.state)
|
throw SwiftfinStore.Errors.existingUser(existingUser.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
||||||
newAccessToken.value = accessToken
|
newAccessToken.value = accessToken
|
||||||
newUser.accessToken = newAccessToken
|
newUser.accessToken = newAccessToken
|
||||||
|
|
||||||
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
else { fatalError("No stored server associated with given state server?") }
|
||||||
|
|
||||||
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
editUserServer.users.insert(newUser)
|
editUserServer.users.insert(newUser)
|
||||||
|
|
||||||
return (editUserServer, newUser, transaction)
|
return (editUserServer, newUser, transaction)
|
||||||
})
|
})
|
||||||
.handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in
|
.handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in
|
||||||
setAuthHeader(with: user.accessToken?.value ?? "")
|
setAuthHeader(with: user.accessToken?.value ?? "")
|
||||||
try? transaction.commitAndWait()
|
try? transaction.commitAndWait()
|
||||||
|
|
||||||
// Fetch for the right queue
|
// Fetch for the right queue
|
||||||
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
||||||
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
||||||
|
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
||||||
|
|
||||||
currentLogin = (server: currentServer.state, user: currentUser.state)
|
currentLogin = (server: currentServer.state, user: currentUser.state)
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||||
})
|
})
|
||||||
|
@ -165,7 +165,7 @@ final class SessionManager {
|
||||||
})
|
})
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
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.uri
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id
|
||||||
|
@ -173,7 +173,7 @@ final class SessionManager {
|
||||||
currentLogin = (server: server, user: user)
|
currentLogin = (server: server, user: user)
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout() {
|
func logout() {
|
||||||
currentLogin = nil
|
currentLogin = nil
|
||||||
JellyfinAPI.basePath = ""
|
JellyfinAPI.basePath = ""
|
||||||
|
@ -181,66 +181,66 @@ final class SessionManager {
|
||||||
SwiftfinStore.Defaults.suite[.lastServerUserID] = nil
|
SwiftfinStore.Defaults.suite[.lastServerUserID] = nil
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func purge() {
|
func purge() {
|
||||||
// Delete all servers
|
// Delete all servers
|
||||||
let servers = fetchServers()
|
let servers = fetchServers()
|
||||||
|
|
||||||
for server in servers {
|
for server in servers {
|
||||||
delete(server: server)
|
delete(server: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete UserDefaults
|
// Delete UserDefaults
|
||||||
SwiftfinStore.Defaults.suite.removeAll()
|
SwiftfinStore.Defaults.suite.removeAll()
|
||||||
|
|
||||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(user: SwiftfinStore.State.User) {
|
func delete(user: SwiftfinStore.State.User) {
|
||||||
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
|
||||||
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)]) else { fatalError("No stored user for state user?")}
|
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)]) else { fatalError("No stored user for state user?")}
|
||||||
_delete(user: storedUser, transaction: nil)
|
_delete(user: storedUser, transaction: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(server: SwiftfinStore.State.Server) {
|
func delete(server: SwiftfinStore.State.Server) {
|
||||||
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
|
||||||
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { fatalError("No stored server for state server?")}
|
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { fatalError("No stored server for state server?")}
|
||||||
_delete(server: storedServer, transaction: nil)
|
_delete(server: storedServer, transaction: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) {
|
private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) {
|
||||||
guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")}
|
guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")}
|
||||||
|
|
||||||
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
||||||
transaction.delete(storedAccessToken)
|
transaction.delete(storedAccessToken)
|
||||||
transaction.delete(user)
|
transaction.delete(user)
|
||||||
try? transaction.commitAndWait()
|
try? transaction.commitAndWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) {
|
private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) {
|
||||||
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
|
||||||
|
|
||||||
for user in server.users {
|
for user in server.users {
|
||||||
_delete(user: user, transaction: transaction)
|
_delete(user: user, transaction: transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.delete(server)
|
transaction.delete(server)
|
||||||
try? transaction.commitAndWait()
|
try? transaction.commitAndWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setAuthHeader(with accessToken: String) {
|
private func setAuthHeader(with accessToken: String) {
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
var deviceName = UIDevice.current.name
|
var deviceName = UIDevice.current.name
|
||||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) })
|
deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) })
|
||||||
|
|
||||||
let platform: String
|
let platform: String
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
platform = "tvOS"
|
platform = "tvOS"
|
||||||
#else
|
#else
|
||||||
platform = "iOS"
|
platform = "iOS"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var header = "MediaBrowser "
|
var header = "MediaBrowser "
|
||||||
header.append("Client=\"Jellyfin \(platform)\", ")
|
header.append("Client=\"Jellyfin \(platform)\", ")
|
||||||
header.append("Device=\"\(deviceName)\", ")
|
header.append("Device=\"\(deviceName)\", ")
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum SwiftfinNotificationCenter {
|
enum SwiftfinNotificationCenter {
|
||||||
|
|
||||||
static let main: NotificationCenter = {
|
static let main: NotificationCenter = {
|
||||||
return NotificationCenter()
|
return NotificationCenter()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
enum Keys {
|
enum Keys {
|
||||||
static let didSignIn = Notification.Name("didSignIn")
|
static let didSignIn = Notification.Name("didSignIn")
|
||||||
static let didSignOut = Notification.Name("didSignOut")
|
static let didSignOut = Notification.Name("didSignOut")
|
||||||
|
|
|
@ -12,12 +12,12 @@ import CoreStore
|
||||||
import Defaults
|
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's
|
||||||
// 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 uri: String
|
||||||
let name: String
|
let name: String
|
||||||
|
@ -25,7 +25,7 @@ enum SwiftfinStore {
|
||||||
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(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) {
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -34,54 +34,54 @@ enum SwiftfinStore {
|
||||||
self.version = version
|
self.version = version
|
||||||
self.userIDs = usersIDs
|
self.userIDs = usersIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
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(uri: "https://www.notaurl.com", name: "Johnny's Tree", id: "123abc", os: "macOS", version: "1.1.1", usersIDs: ["1", "2"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
let username: String
|
let username: String
|
||||||
let id: String
|
let id: String
|
||||||
let serverID: String
|
let serverID: String
|
||||||
let accessToken: String
|
let accessToken: String
|
||||||
|
|
||||||
fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
|
fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
|
||||||
self.username = username
|
self.username = username
|
||||||
self.id = id
|
self.id = id
|
||||||
self.serverID = serverID
|
self.serverID = serverID
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Models
|
// MARK: Models
|
||||||
enum Models {
|
enum Models {
|
||||||
|
|
||||||
final class StoredServer: CoreStoreObject {
|
final class StoredServer: CoreStoreObject {
|
||||||
|
|
||||||
@Field.Stored("uri")
|
@Field.Stored("uri")
|
||||||
var uri: String = ""
|
var uri: String = ""
|
||||||
|
|
||||||
@Field.Stored("name")
|
@Field.Stored("name")
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
|
|
||||||
@Field.Stored("id")
|
@Field.Stored("id")
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
|
|
||||||
@Field.Stored("os")
|
@Field.Stored("os")
|
||||||
var os: String = ""
|
var os: String = ""
|
||||||
|
|
||||||
@Field.Stored("version")
|
@Field.Stored("version")
|
||||||
var version: String = ""
|
var version: String = ""
|
||||||
|
|
||||||
@Field.Relationship("users", inverse: \StoredUser.$server)
|
@Field.Relationship("users", inverse: \StoredUser.$server)
|
||||||
var users: Set<StoredUser>
|
var users: Set<StoredUser>
|
||||||
|
|
||||||
var state: State.Server {
|
var state: State.Server {
|
||||||
return State.Server(uri: uri,
|
return State.Server(uri: uri,
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -91,24 +91,24 @@ enum SwiftfinStore {
|
||||||
usersIDs: users.map({ $0.id }))
|
usersIDs: users.map({ $0.id }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StoredUser: CoreStoreObject {
|
final class StoredUser: CoreStoreObject {
|
||||||
|
|
||||||
@Field.Stored("username")
|
@Field.Stored("username")
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
|
|
||||||
@Field.Stored("id")
|
@Field.Stored("id")
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
|
|
||||||
@Field.Stored("appleTVID")
|
@Field.Stored("appleTVID")
|
||||||
var appleTVID: String = ""
|
var appleTVID: String = ""
|
||||||
|
|
||||||
@Field.Relationship("server")
|
@Field.Relationship("server")
|
||||||
var server: StoredServer?
|
var server: StoredServer?
|
||||||
|
|
||||||
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
|
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
|
||||||
var accessToken: StoredAccessToken?
|
var accessToken: StoredAccessToken?
|
||||||
|
|
||||||
var state: State.User {
|
var state: State.User {
|
||||||
guard let server = server else { fatalError("No server associated with user") }
|
guard let server = server else { fatalError("No server associated with user") }
|
||||||
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
|
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
|
||||||
|
@ -118,23 +118,23 @@ enum SwiftfinStore {
|
||||||
accessToken: accessToken.value)
|
accessToken: accessToken.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StoredAccessToken: CoreStoreObject {
|
final class StoredAccessToken: CoreStoreObject {
|
||||||
|
|
||||||
@Field.Stored("value")
|
@Field.Stored("value")
|
||||||
var value: String = ""
|
var value: String = ""
|
||||||
|
|
||||||
@Field.Relationship("user")
|
@Field.Relationship("user")
|
||||||
var user: StoredUser?
|
var user: StoredUser?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Errors
|
// MARK: Errors
|
||||||
enum Errors {
|
enum Errors {
|
||||||
case existingServer(State.Server)
|
case existingServer(State.Server)
|
||||||
case existingUser(State.User)
|
case existingUser(State.User)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: dataStack
|
// MARK: dataStack
|
||||||
static let dataStack: DataStack = {
|
static let dataStack: DataStack = {
|
||||||
let schema = CoreStoreSchema(modelVersion: "V1",
|
let schema = CoreStoreSchema(modelVersion: "V1",
|
||||||
|
@ -148,7 +148,7 @@ enum SwiftfinStore {
|
||||||
"Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db],
|
"Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db],
|
||||||
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
|
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
|
||||||
])
|
])
|
||||||
|
|
||||||
let _dataStack = DataStack(schema)
|
let _dataStack = DataStack(schema)
|
||||||
try! _dataStack.addStorageAndWait(
|
try! _dataStack.addStorageAndWait(
|
||||||
SQLiteStore(
|
SQLiteStore(
|
||||||
|
@ -162,7 +162,7 @@ enum SwiftfinStore {
|
||||||
|
|
||||||
// MARK: LocalizedError
|
// MARK: LocalizedError
|
||||||
extension SwiftfinStore.Errors: LocalizedError {
|
extension SwiftfinStore.Errors: LocalizedError {
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .existingServer(_):
|
case .existingServer(_):
|
||||||
|
@ -171,7 +171,7 @@ extension SwiftfinStore.Errors: LocalizedError {
|
||||||
return "Existing User"
|
return "Existing User"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .existingServer(let server):
|
case .existingServer(let server):
|
||||||
|
|
|
@ -11,9 +11,9 @@ import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension SwiftfinStore {
|
extension SwiftfinStore {
|
||||||
|
|
||||||
enum Defaults {
|
enum Defaults {
|
||||||
|
|
||||||
static let suite: UserDefaults = {
|
static let suite: UserDefaults = {
|
||||||
return UserDefaults(suiteName: "swiftfinstore-defaults")!
|
return UserDefaults(suiteName: "swiftfinstore-defaults")!
|
||||||
}()
|
}()
|
||||||
|
@ -22,7 +22,7 @@ extension SwiftfinStore {
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
|
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
|
||||||
|
|
||||||
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
|
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
|
||||||
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
||||||
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class BasicAppSettingsViewModel: ViewModel {
|
final class BasicAppSettingsViewModel: ViewModel {
|
||||||
|
|
||||||
let appearances = AppAppearance.allCases
|
let appearances = AppAppearance.allCases
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
SessionManager.main.purge()
|
SessionManager.main.purge()
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,12 @@ import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
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
|
||||||
private let discovery = ServerDiscovery()
|
private let discovery = ServerDiscovery()
|
||||||
|
|
||||||
var alertTitle: String {
|
var alertTitle: String {
|
||||||
var message: String = ""
|
var message: String = ""
|
||||||
if errorMessage?.code != ErrorMessage.noShowErrorCode {
|
if errorMessage?.code != ErrorMessage.noShowErrorCode {
|
||||||
|
@ -64,12 +64,12 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelConnection() {
|
func cancelConnection() {
|
||||||
for cancellable in cancellables {
|
for cancellable in cancellables {
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,9 @@ final class HomeViewModel: ViewModel {
|
||||||
self.handleAPIRequestError(completion: completion)
|
self.handleAPIRequestError(completion: completion)
|
||||||
}
|
}
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
|
||||||
var newLibraries: [BaseItemDto] = []
|
var newLibraries: [BaseItemDto] = []
|
||||||
|
|
||||||
response.items!.forEach { item in
|
response.items!.forEach { item in
|
||||||
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
|
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
|
||||||
if item.collectionType == "movies" || item.collectionType == "tvshows" {
|
if item.collectionType == "movies" || item.collectionType == "tvshows" {
|
||||||
|
@ -60,13 +60,13 @@ final class HomeViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
|
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
|
||||||
|
|
||||||
for excludeID in excludeIDs {
|
for excludeID in excludeIDs {
|
||||||
newLibraries.removeAll { library in
|
newLibraries.removeAll { library in
|
||||||
return library.id == excludeID
|
return library.id == excludeID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.libraries = newLibraries
|
self.libraries = newLibraries
|
||||||
})
|
})
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
|
@ -88,7 +88,7 @@ final class HomeViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
|
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
|
||||||
|
|
||||||
self.resumeItems = response.items ?? []
|
self.resumeItems = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -105,7 +105,7 @@ final class HomeViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
|
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
|
||||||
|
|
||||||
self.nextUpItems = response.items ?? []
|
self.nextUpItems = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
class ItemViewModel: ViewModel {
|
class ItemViewModel: ViewModel {
|
||||||
|
|
||||||
@Published var item: BaseItemDto
|
@Published var item: BaseItemDto
|
||||||
@Published var playButtonItem: BaseItemDto?
|
@Published var playButtonItem: BaseItemDto?
|
||||||
@Published var similarItems: [BaseItemDto] = []
|
@Published var similarItems: [BaseItemDto] = []
|
||||||
|
@ -20,32 +20,32 @@ class ItemViewModel: ViewModel {
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
switch item.itemType {
|
switch item.itemType {
|
||||||
case .episode, .movie:
|
case .episode, .movie:
|
||||||
self.playButtonItem = item
|
self.playButtonItem = item
|
||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
|
|
||||||
isFavorited = item.userData?.isFavorite ?? false
|
isFavorited = item.userData?.isFavorite ?? false
|
||||||
isWatched = item.userData?.played ?? false
|
isWatched = item.userData?.played ?? false
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
getSimilarItems()
|
getSimilarItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
func playButtonText() -> String {
|
func playButtonText() -> String {
|
||||||
return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString()
|
return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemDisplayName() -> String {
|
func getItemDisplayName() -> String {
|
||||||
return item.name ?? ""
|
return item.name ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldDisplayRuntime() -> Bool {
|
func shouldDisplayRuntime() -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSimilarItems() {
|
func getSimilarItems() {
|
||||||
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
|
|
@ -36,7 +36,7 @@ final class LibraryViewModel: ViewModel {
|
||||||
|
|
||||||
// temp
|
// temp
|
||||||
@Published var filters: LibraryFilters
|
@Published var filters: LibraryFilters
|
||||||
|
|
||||||
private let columns: Int
|
private let columns: Int
|
||||||
private var libraries = [BaseItemDto]()
|
private var libraries = [BaseItemDto]()
|
||||||
|
|
||||||
|
@ -64,11 +64,10 @@ final class LibraryViewModel: ViewModel {
|
||||||
self.columns = columns
|
self.columns = columns
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
|
||||||
$filters
|
$filters
|
||||||
.sink(receiveValue: requestItems(with:))
|
.sink(receiveValue: requestItems(with:))
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestItems(with filters: LibraryFilters) {
|
func requestItems(with filters: LibraryFilters) {
|
||||||
|
@ -147,7 +146,7 @@ final class LibraryViewModel: ViewModel {
|
||||||
currentPage -= 1
|
currentPage -= 1
|
||||||
requestItems(with: filters)
|
requestItems(with: filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
|
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
|
||||||
guard itemList.count > 0 else { return [] }
|
guard itemList.count > 0 else { return [] }
|
||||||
let rowCount = itemList.count / columns
|
let rowCount = itemList.count / columns
|
||||||
|
|
|
@ -14,19 +14,19 @@ import Stinsen
|
||||||
import SwiftUICollection
|
import SwiftUICollection
|
||||||
|
|
||||||
final class MovieLibrariesViewModel: ViewModel {
|
final class MovieLibrariesViewModel: ViewModel {
|
||||||
|
|
||||||
@Published var rows = [LibraryRow]()
|
@Published var rows = [LibraryRow]()
|
||||||
@Published var totalPages = 0
|
@Published var totalPages = 0
|
||||||
@Published var currentPage = 0
|
@Published var currentPage = 0
|
||||||
@Published var hasNextPage = false
|
@Published var hasNextPage = false
|
||||||
@Published var hasPreviousPage = false
|
@Published var hasPreviousPage = false
|
||||||
|
|
||||||
private var libraries = [BaseItemDto]()
|
private var libraries = [BaseItemDto]()
|
||||||
private let columns: Int
|
private let columns: Int
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var router: MovieLibrariesCoordinator.Router?
|
var router: MovieLibrariesCoordinator.Router?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
columns: Int = 7
|
columns: Int = 7
|
||||||
) {
|
) {
|
||||||
|
@ -35,9 +35,9 @@ final class MovieLibrariesViewModel: ViewModel {
|
||||||
|
|
||||||
requestLibraries()
|
requestLibraries()
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestLibraries() {
|
func requestLibraries() {
|
||||||
|
|
||||||
UserViewsAPI.getUserViews(
|
UserViewsAPI.getUserViews(
|
||||||
userId: SessionManager.main.currentLogin.user.id)
|
userId: SessionManager.main.currentLogin.user.id)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -60,7 +60,7 @@ final class MovieLibrariesViewModel: ViewModel {
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateRows() -> [LibraryRow] {
|
private func calculateRows() -> [LibraryRow] {
|
||||||
guard libraries.count > 0 else { return [] }
|
guard libraries.count > 0 else { return [] }
|
||||||
let rowCount = libraries.count / columns
|
let rowCount = libraries.count / columns
|
||||||
|
|
|
@ -70,7 +70,7 @@ final class SeasonItemViewModel: ItemViewModel {
|
||||||
playButtonItem = firstEpisode
|
playButtonItem = firstEpisode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeToSeriesItem() {
|
func routeToSeriesItem() {
|
||||||
guard let id = item.seriesId else { return }
|
guard let id = item.seriesId else { return }
|
||||||
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
|
||||||
|
|
|
@ -21,19 +21,19 @@ final class SeriesItemViewModel: ItemViewModel {
|
||||||
requestSeasons()
|
requestSeasons()
|
||||||
getNextUp()
|
getNextUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func playButtonText() -> String {
|
override func playButtonText() -> String {
|
||||||
guard let playButtonItem = playButtonItem else { return L10n.play }
|
guard let playButtonItem = playButtonItem else { return L10n.play }
|
||||||
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
|
||||||
return episodeLocator
|
return episodeLocator
|
||||||
}
|
}
|
||||||
|
|
||||||
override func shouldDisplayRuntime() -> Bool {
|
override func shouldDisplayRuntime() -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getNextUp() {
|
private func getNextUp() {
|
||||||
|
|
||||||
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true)
|
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
|
|
@ -11,11 +11,11 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class ServerListViewModel: ObservableObject {
|
class ServerListViewModel: ObservableObject {
|
||||||
|
|
||||||
@Published var servers: [SwiftfinStore.State.Server] = []
|
@Published var servers: [SwiftfinStore.State.Server] = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
// Oct. 15, 2021
|
// Oct. 15, 2021
|
||||||
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
|
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
|
||||||
// Feature request issue: https://github.com/rundfunk47/stinsen/issues/33
|
// Feature request issue: https://github.com/rundfunk47/stinsen/issues/33
|
||||||
|
@ -23,11 +23,11 @@ class ServerListViewModel: ObservableObject {
|
||||||
let nc = SwiftfinNotificationCenter.main
|
let nc = SwiftfinNotificationCenter.main
|
||||||
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchServers() {
|
func fetchServers() {
|
||||||
self.servers = SessionManager.main.fetchServers()
|
self.servers = SessionManager.main.fetchServers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func userTextFor(server: SwiftfinStore.State.Server) -> String {
|
func userTextFor(server: SwiftfinStore.State.Server) -> String {
|
||||||
if server.userIDs.count == 1 {
|
if server.userIDs.count == 1 {
|
||||||
return "1 user"
|
return "1 user"
|
||||||
|
@ -35,12 +35,12 @@ class ServerListViewModel: ObservableObject {
|
||||||
return "\(server.userIDs.count) users"
|
return "\(server.userIDs.count) users"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(server: SwiftfinStore.State.Server) {
|
func remove(server: SwiftfinStore.State.Server) {
|
||||||
SessionManager.main.delete(server: server)
|
SessionManager.main.delete(server: server)
|
||||||
fetchServers()
|
fetchServers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func didPurge() {
|
@objc private func didPurge() {
|
||||||
fetchServers()
|
fetchServers()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,19 +14,19 @@ import Stinsen
|
||||||
import SwiftUICollection
|
import SwiftUICollection
|
||||||
|
|
||||||
final class TVLibrariesViewModel: ViewModel {
|
final class TVLibrariesViewModel: ViewModel {
|
||||||
|
|
||||||
@Published var rows = [LibraryRow]()
|
@Published var rows = [LibraryRow]()
|
||||||
@Published var totalPages = 0
|
@Published var totalPages = 0
|
||||||
@Published var currentPage = 0
|
@Published var currentPage = 0
|
||||||
@Published var hasNextPage = false
|
@Published var hasNextPage = false
|
||||||
@Published var hasPreviousPage = false
|
@Published var hasPreviousPage = false
|
||||||
|
|
||||||
private var libraries = [BaseItemDto]()
|
private var libraries = [BaseItemDto]()
|
||||||
private let columns: Int
|
private let columns: Int
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var router: TVLibrariesCoordinator.Router?
|
var router: TVLibrariesCoordinator.Router?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
columns: Int = 7
|
columns: Int = 7
|
||||||
) {
|
) {
|
||||||
|
@ -35,9 +35,9 @@ final class TVLibrariesViewModel: ViewModel {
|
||||||
|
|
||||||
requestLibraries()
|
requestLibraries()
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestLibraries() {
|
func requestLibraries() {
|
||||||
|
|
||||||
UserViewsAPI.getUserViews(
|
UserViewsAPI.getUserViews(
|
||||||
userId: SessionManager.main.currentLogin.user.id)
|
userId: SessionManager.main.currentLogin.user.id)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
|
@ -60,7 +60,7 @@ final class TVLibrariesViewModel: ViewModel {
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateRows() -> [LibraryRow] {
|
private func calculateRows() -> [LibraryRow] {
|
||||||
guard libraries.count > 0 else { return [] }
|
guard libraries.count > 0 else { return [] }
|
||||||
let rowCount = libraries.count / columns
|
let rowCount = libraries.count / columns
|
||||||
|
|
|
@ -11,24 +11,24 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class UserListViewModel: ViewModel {
|
class UserListViewModel: ViewModel {
|
||||||
|
|
||||||
@Published var users: [SwiftfinStore.State.User] = []
|
@Published var users: [SwiftfinStore.State.User] = []
|
||||||
|
|
||||||
let server: SwiftfinStore.State.Server
|
let server: SwiftfinStore.State.Server
|
||||||
|
|
||||||
init(server: SwiftfinStore.State.Server) {
|
init(server: SwiftfinStore.State.Server) {
|
||||||
self.server = server
|
self.server = server
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUsers() {
|
func fetchUsers() {
|
||||||
self.users = SessionManager.main.fetchUsers(for: server)
|
self.users = SessionManager.main.fetchUsers(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(user: SwiftfinStore.State.User) {
|
func login(user: SwiftfinStore.State.User) {
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
SessionManager.main.loginUser(server: server, user: user)
|
SessionManager.main.loginUser(server: server, user: user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(user: SwiftfinStore.State.User) {
|
func remove(user: SwiftfinStore.State.User) {
|
||||||
SessionManager.main.delete(user: user)
|
SessionManager.main.delete(user: user)
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
|
|
|
@ -12,14 +12,14 @@ import Foundation
|
||||||
import Stinsen
|
import Stinsen
|
||||||
|
|
||||||
final class UserSignInViewModel: ViewModel {
|
final class UserSignInViewModel: ViewModel {
|
||||||
|
|
||||||
@RouterObject var router: UserSignInCoordinator.Router?
|
@RouterObject var router: UserSignInCoordinator.Router?
|
||||||
let server: SwiftfinStore.State.Server
|
let server: SwiftfinStore.State.Server
|
||||||
|
|
||||||
init(server: SwiftfinStore.State.Server) {
|
init(server: SwiftfinStore.State.Server) {
|
||||||
self.server = server
|
self.server = server
|
||||||
}
|
}
|
||||||
|
|
||||||
var alertTitle: String {
|
var alertTitle: String {
|
||||||
var message: String = ""
|
var message: String = ""
|
||||||
if errorMessage?.code != ErrorMessage.noShowErrorCode {
|
if errorMessage?.code != ErrorMessage.noShowErrorCode {
|
||||||
|
@ -28,27 +28,27 @@ final class UserSignInViewModel: ViewModel {
|
||||||
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
|
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
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.uri)\"", 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)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
|
||||||
completion: completion)
|
completion: completion)
|
||||||
} receiveValue: { _ in
|
} receiveValue: { _ in
|
||||||
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelSignIn() {
|
func cancelSignIn() {
|
||||||
for cancellable in cancellables {
|
for cancellable in cancellables {
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ViewModel: ObservableObject {
|
||||||
break
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
|
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
|
||||||
|
|
||||||
switch error {
|
switch error {
|
||||||
case is ErrorResponse:
|
case is ErrorResponse:
|
||||||
let networkError: NetworkError
|
let networkError: NetworkError
|
||||||
|
@ -52,7 +52,7 @@ class ViewModel: ObservableObject {
|
||||||
self.errorMessage = networkError.errorMessage
|
self.errorMessage = networkError.errorMessage
|
||||||
|
|
||||||
networkError.logMessage()
|
networkError.logMessage()
|
||||||
|
|
||||||
case is SwiftfinStore.Errors:
|
case is SwiftfinStore.Errors:
|
||||||
let swiftfinError = error as! SwiftfinStore.Errors
|
let swiftfinError = error as! SwiftfinStore.Errors
|
||||||
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
||||||
|
@ -61,7 +61,7 @@ class ViewModel: ObservableObject {
|
||||||
logConstructor: logConstructor)
|
logConstructor: logConstructor)
|
||||||
self.errorMessage = errorMessage
|
self.errorMessage = errorMessage
|
||||||
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
|
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
|
||||||
title: "Generic Error",
|
title: "Generic Error",
|
||||||
|
|
|
@ -19,13 +19,13 @@ struct ImageView: View {
|
||||||
self.blurhash = bh
|
self.blurhash = bh
|
||||||
self.failureInitials = failureInitials
|
self.failureInitials = failureInitials
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var placeholderImage: some View {
|
private var placeholderImage: some View {
|
||||||
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8)) ?? UIImage(blurHash: "001fC^", size: CGSize(width: 8, height: 8))!)
|
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 8, height: 8)) ?? UIImage(blurHash: "001fC^", size: CGSize(width: 8, height: 8))!)
|
||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var failureImage: some View {
|
private var failureImage: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
|
@ -365,7 +365,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -376,7 +376,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -391,7 +391,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -403,7 +403,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -416,7 +416,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
NextUpEntryView(entry: .init(date: Date(),
|
NextUpEntryView(entry: .init(date: Date(),
|
||||||
items: [
|
items: [
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -426,7 +426,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
|
Loading…
Reference in New Issue