swiftlint autocorrect

This commit is contained in:
PangMo5 2021-11-08 03:53:42 +09:00
parent 6307ae4e26
commit 923af3f013
80 changed files with 396 additions and 404 deletions

View File

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

View File

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

View File

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

View File

@ -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 ?? ""))
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
} }
} }

View File

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

View File

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

View File

@ -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()
} }

View File

@ -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?()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
} }

View File

@ -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())
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ extension String {
return "\(padString)\(self)" return "\(padString)\(self)"
} }
var text: Text { var text: Text {
Text(self) Text(self)
} }

View File

@ -18,11 +18,8 @@ enum DetailItemType: String {
} }
struct DetailItem { struct DetailItem {
let baseItem: BaseItemDto let baseItem: BaseItemDto
let type: DetailItemType let type: DetailItemType
} }

View File

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

View File

@ -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() {

View File

@ -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)\", ")

View File

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

View File

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

View File

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

View File

@ -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()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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