diff --git a/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift b/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift index 7adcc3f0..241557f9 100644 --- a/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift +++ b/JellyfinPlayer tvOS/Views/BasicAppSettingsView.swift @@ -12,13 +12,13 @@ import Stinsen import SwiftUI struct BasicAppSettingsView: View { - + @EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router @ObservedObject var viewModel: BasicAppSettingsViewModel @State var resetTapped: Bool = false - + @Default(.appAppearance) var appAppearance - + var body: some View { Form { Section { @@ -32,7 +32,7 @@ struct BasicAppSettingsView: View { } header: { L10n.accessibility.text } - + Button { resetTapped = true } label: { diff --git a/JellyfinPlayer tvOS/Views/ConnectToServerView.swift b/JellyfinPlayer tvOS/Views/ConnectToServerView.swift index dbffe5f9..f47ff0b5 100644 --- a/JellyfinPlayer tvOS/Views/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/Views/ConnectToServerView.swift @@ -10,10 +10,10 @@ import SwiftUI import Stinsen struct ConnectToServerView: View { - + @StateObject var viewModel = ConnectToServerViewModel() @State var uri = "" - + var body: some View { List { Section { @@ -36,7 +36,7 @@ struct ConnectToServerView: View { } header: { Text("Connect to a Jellyfin server") } - + Section(header: L10n.localServers.text) { if viewModel.searching { ProgressView() diff --git a/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift index f9ccf27a..02a85f82 100644 --- a/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/Views/ContinueWatchingView.swift @@ -16,7 +16,7 @@ struct ContinueWatchingView: View { @Namespace private var namespace var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() - + var body: some View { VStack(alignment: .leading) { if items.count > 0 { diff --git a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift index 5f8353cf..32e707ac 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift @@ -39,7 +39,7 @@ struct ItemView: View { SeasonItemView(viewModel: .init(item: item)) } else if item.type == "Episode" { EpisodeItemView(viewModel: .init(item: item)) - } else { + } else { Text(L10n.notImplementedYetWithType(item.type ?? "")) } } diff --git a/JellyfinPlayer tvOS/Views/LibraryFilterView.swift b/JellyfinPlayer tvOS/Views/LibraryFilterView.swift index a10a0cdc..cf77babe 100644 --- a/JellyfinPlayer tvOS/Views/LibraryFilterView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryFilterView.swift @@ -10,7 +10,7 @@ import Stinsen import SwiftUI struct LibraryFilterView: View { - + @EnvironmentObject var filterRouter: FilterCoordinator.Router @Binding var filters: LibraryFilters var parentId: String = "" diff --git a/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift index 86298af2..ab87ce24 100644 --- a/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift +++ b/JellyfinPlayer tvOS/Views/MovieLibrariesView.swift @@ -14,7 +14,7 @@ struct MovieLibrariesView: View { @EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router @StateObject var viewModel: MovieLibrariesViewModel var title: String - + var body: some View { if viewModel.isLoading == true { ProgressView() diff --git a/JellyfinPlayer tvOS/Views/NextUpView.swift b/JellyfinPlayer tvOS/Views/NextUpView.swift index 6ad02d5e..aa68e8ac 100644 --- a/JellyfinPlayer tvOS/Views/NextUpView.swift +++ b/JellyfinPlayer tvOS/Views/NextUpView.swift @@ -13,7 +13,7 @@ import Stinsen struct NextUpView: View { var items: [BaseItemDto] - + var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve() var body: some View { diff --git a/JellyfinPlayer tvOS/Views/ServerListView.swift b/JellyfinPlayer tvOS/Views/ServerListView.swift index cb653b84..30ad391c 100644 --- a/JellyfinPlayer tvOS/Views/ServerListView.swift +++ b/JellyfinPlayer tvOS/Views/ServerListView.swift @@ -11,10 +11,10 @@ import CoreStore import SwiftUI struct ServerListView: View { - + @EnvironmentObject var serverListRouter: ServerListCoordinator.Router @ObservedObject var viewModel: ServerListViewModel - + @ViewBuilder private var listView: some View { ScrollView { @@ -27,22 +27,22 @@ struct ServerListView: View { Image(systemName: "server.rack") .font(.system(size: 72)) .foregroundColor(.primary) - + VStack(alignment: .leading, spacing: 5) { Text(server.name) .font(.title2) .foregroundColor(.primary) - + Text(server.uri) .font(.footnote) .disabled(true) .foregroundColor(.secondary) - + Text(viewModel.userTextFor(server: server)) .font(.footnote) .foregroundColor(.primary) } - + Spacer() } } @@ -60,7 +60,7 @@ struct ServerListView: View { } .padding(.top, 50) } - + @ViewBuilder private var noServerView: some View { VStack { @@ -68,7 +68,7 @@ struct ServerListView: View { .frame(minWidth: 50, maxWidth: 500) .multilineTextAlignment(.center) .font(.callout) - + Button { serverListRouter.route(to: \.connectToServer) } label: { @@ -79,7 +79,7 @@ struct ServerListView: View { .padding(.top, 40) } } - + @ViewBuilder private var innerBody: some View { if viewModel.servers.isEmpty { @@ -89,7 +89,7 @@ struct ServerListView: View { listView } } - + @ViewBuilder private var trailingToolbarContent: some View { if viewModel.servers.isEmpty { @@ -109,7 +109,7 @@ struct ServerListView: View { } } } - + var body: some View { innerBody .navigationTitle("Servers") diff --git a/JellyfinPlayer tvOS/Views/TVLibrariesView.swift b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift index fbf58d20..25c06ceb 100644 --- a/JellyfinPlayer tvOS/Views/TVLibrariesView.swift +++ b/JellyfinPlayer tvOS/Views/TVLibrariesView.swift @@ -14,7 +14,7 @@ struct TVLibrariesView: View { @EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router @StateObject var viewModel: TVLibrariesViewModel var title: String - + var body: some View { if viewModel.isLoading == true { ProgressView() diff --git a/JellyfinPlayer tvOS/Views/UserListView.swift b/JellyfinPlayer tvOS/Views/UserListView.swift index 85bcbe52..bc7c93f9 100644 --- a/JellyfinPlayer tvOS/Views/UserListView.swift +++ b/JellyfinPlayer tvOS/Views/UserListView.swift @@ -10,10 +10,10 @@ import SwiftUI struct UserListView: View { - + @EnvironmentObject var userListRouter: UserListCoordinator.Router @ObservedObject var viewModel: UserListViewModel - + @ViewBuilder private var listView: some View { ScrollView { @@ -25,9 +25,9 @@ struct UserListView: View { HStack { Text(user.username) .font(.title2) - + Spacer() - + if viewModel.isLoading { ProgressView() } @@ -47,7 +47,7 @@ struct UserListView: View { } .padding(.top, 50) } - + @ViewBuilder private var noUserView: some View { VStack { @@ -55,7 +55,7 @@ struct UserListView: View { .frame(minWidth: 50, maxWidth: 500) .multilineTextAlignment(.center) .font(.callout) - + Button { userListRouter.route(to: \.userSignIn, viewModel.server) } label: { @@ -66,7 +66,7 @@ struct UserListView: View { .padding(.top, 40) } } - + @ViewBuilder private var innerBody: some View { if viewModel.users.isEmpty { @@ -76,7 +76,7 @@ struct UserListView: View { listView } } - + @ViewBuilder private var toolbarContent: some View { if viewModel.users.isEmpty { @@ -91,7 +91,7 @@ struct UserListView: View { } } } - + var body: some View { innerBody .navigationTitle(viewModel.server.name) diff --git a/JellyfinPlayer tvOS/Views/UserSignInView.swift b/JellyfinPlayer tvOS/Views/UserSignInView.swift index 9780beb0..2e78d2b5 100644 --- a/JellyfinPlayer tvOS/Views/UserSignInView.swift +++ b/JellyfinPlayer tvOS/Views/UserSignInView.swift @@ -11,23 +11,23 @@ import SwiftUI import Stinsen struct UserSignInView: View { - + @ObservedObject var viewModel: UserSignInViewModel @State private var username: String = "" @State private var password: String = "" - + var body: some View { Form { - + Section { TextField(L10n.username, text: $username) .disableAutocorrection(true) .autocapitalization(.none) - + SecureField(L10n.password, text: $password) .disableAutocorrection(true) .autocapitalization(.none) - + Button { viewModel.login(username: username, password: password) } label: { diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift index 0c4c5bae..e680ddaa 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift @@ -138,7 +138,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - + let currentUser = SessionManager.main.currentLogin.user let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) diff --git a/JellyfinPlayer/App/AppDelegate.swift b/JellyfinPlayer/App/AppDelegate.swift index d41c406f..21701dd2 100644 --- a/JellyfinPlayer/App/AppDelegate.swift +++ b/JellyfinPlayer/App/AppDelegate.swift @@ -12,12 +12,12 @@ import UIKit class AppDelegate: NSObject, UIApplicationDelegate { 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 - let _ = SwiftfinStore.dataStack - + _ = SwiftfinStore.dataStack + return true } diff --git a/JellyfinPlayer/App/EmailHelper.swift b/JellyfinPlayer/App/EmailHelper.swift index 5c054d9f..87fd8523 100644 --- a/JellyfinPlayer/App/EmailHelper.swift +++ b/JellyfinPlayer/App/EmailHelper.swift @@ -11,9 +11,9 @@ import SwiftUI import MessageUI class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { - + public static let shared = EmailHelper() - + override private init() { } func sendLogs(logURL: URL) { diff --git a/JellyfinPlayer/App/JellyfinPlayerApp.swift b/JellyfinPlayer/App/JellyfinPlayerApp.swift index 27440a56..2d58ea07 100644 --- a/JellyfinPlayer/App/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/App/JellyfinPlayerApp.swift @@ -13,7 +13,7 @@ import SwiftUI // MARK: JellyfinPlayerApp @main struct JellyfinPlayerApp: App { - + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Default(.appAppearance) var appAppearance @@ -35,7 +35,7 @@ struct JellyfinPlayerApp: App { } } } - + private func setupAppearance() { UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style } diff --git a/JellyfinPlayer/AppURLHandler/AppURLHandler.swift b/JellyfinPlayer/AppURLHandler/AppURLHandler.swift index 4c64b916..7f745487 100644 --- a/JellyfinPlayer/AppURLHandler/AppURLHandler.swift +++ b/JellyfinPlayer/AppURLHandler/AppURLHandler.swift @@ -77,8 +77,7 @@ extension AppURLHandler { // /Users/{UserID}/Items/{ItemID} if url.pathComponents[safe: 2]?.lowercased() == "items", 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. getItem(userID: userID, itemID: itemID) { item in guard let item = item else { return } diff --git a/JellyfinPlayer/Components/EpisodeCardVStackView.swift b/JellyfinPlayer/Components/EpisodeCardVStackView.swift index b7f2bac8..aee29489 100644 --- a/JellyfinPlayer/Components/EpisodeCardVStackView.swift +++ b/JellyfinPlayer/Components/EpisodeCardVStackView.swift @@ -11,10 +11,10 @@ import SwiftUI import JellyfinAPI struct EpisodeCardVStackView: View { - + let items: [BaseItemDto] let selectedAction: (BaseItemDto) -> Void - + private func buildCardOverlayView(item: BaseItemDto) -> some View { HStack { ZStack { @@ -30,7 +30,7 @@ struct EpisodeCardVStackView: View { .padding(.leading, 2) .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) .opacity(1) - + ZStack { if item.userData?.played ?? false { Image(systemName: "circle.fill") @@ -42,7 +42,7 @@ struct EpisodeCardVStackView: View { .opacity(1) } } - + var body: some View { VStack { ForEach(items, id: \.id) { item in @@ -50,7 +50,7 @@ struct EpisodeCardVStackView: View { selectedAction(item) } label: { HStack { - + // MARK: Image ImageView(src: item.getPrimaryImage(maxWidth: 150), bh: item.getPrimaryImageBlurHash(), @@ -65,37 +65,37 @@ struct EpisodeCardVStackView: View { .padding(0), alignment: .bottomLeading ) .overlay(buildCardOverlayView(item: item), alignment: .topTrailing) - + VStack(alignment: .leading) { - + // MARK: Title Text(item.title) .font(.subheadline) .fontWeight(.medium) .foregroundColor(.primary) .lineLimit(2) - + HStack { Text(item.getEpisodeLocator() ?? "") .font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) - + Text(item.getItemRuntime()) .font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) - + Spacer() } - + // MARK: Overview Text(item.overview ?? "") .font(.footnote) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .lineLimit(4) - + Spacer() } } diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift index d9ce7769..d66cf279 100644 --- a/JellyfinPlayer/Components/PillHStackView.swift +++ b/JellyfinPlayer/Components/PillHStackView.swift @@ -10,12 +10,12 @@ import SwiftUI struct PillHStackView: View { - + let title: String let items: [ItemType] // let navigationView: (ItemType) -> NavigationView let selectedAction: (ItemType) -> Void - + var body: some View { VStack(alignment: .leading) { Text(title) @@ -23,7 +23,7 @@ struct PillHStackView: View { .fontWeight(.semibold) .padding(.top, 3) .padding(.leading, 16) - + ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(items, id: \.title) { item in diff --git a/JellyfinPlayer/Components/PortraitHStackView.swift b/JellyfinPlayer/Components/PortraitHStackView.swift index db9febec..ec2e2ce9 100644 --- a/JellyfinPlayer/Components/PortraitHStackView.swift +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -10,13 +10,13 @@ import SwiftUI struct PortraitImageHStackView: View { - + let items: [ItemType] let maxWidth: Int let horizontalAlignment: HorizontalAlignment let topBarView: () -> TopBarView let selectedAction: (ItemType) -> Void - + init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) { self.items = items self.maxWidth = maxWidth @@ -24,18 +24,18 @@ struct PortraitImageHStackView Void)? - + @objc func didRefresh() { guard let refreshControl = refreshControl else { return } refreshAction?() diff --git a/JellyfinPlayer/Views/BasicAppSettingsView.swift b/JellyfinPlayer/Views/BasicAppSettingsView.swift index 0c57dd42..711454b2 100644 --- a/JellyfinPlayer/Views/BasicAppSettingsView.swift +++ b/JellyfinPlayer/Views/BasicAppSettingsView.swift @@ -12,14 +12,14 @@ import Stinsen import SwiftUI struct BasicAppSettingsView: View { - + @EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router @ObservedObject var viewModel: BasicAppSettingsViewModel @State var resetTapped: Bool = false - + @Default(.appAppearance) var appAppearance @Default(.defaultHTTPScheme) var defaultHTTPScheme - + var body: some View { Form { Section { @@ -33,7 +33,7 @@ struct BasicAppSettingsView: View { } header: { L10n.accessibility.text } - + Section { Picker("Default Scheme", selection: $defaultHTTPScheme) { ForEach(HTTPScheme.allCases, id: \.self) { scheme in @@ -43,7 +43,7 @@ struct BasicAppSettingsView: View { } header: { Text("Networking") } - + Button { resetTapped = true } label: { diff --git a/JellyfinPlayer/Views/ConnectToServerView.swift b/JellyfinPlayer/Views/ConnectToServerView.swift index f91b9d85..d6c74601 100644 --- a/JellyfinPlayer/Views/ConnectToServerView.swift +++ b/JellyfinPlayer/Views/ConnectToServerView.swift @@ -11,12 +11,12 @@ import Stinsen import SwiftUI struct ConnectToServerView: View { - + @StateObject var viewModel: ConnectToServerViewModel @State var uri = "" - + @Default(.defaultHTTPScheme) var defaultHTTPScheme - + var body: some View { List { Section { @@ -29,7 +29,7 @@ struct ConnectToServerView: View { uri = "\(defaultHTTPScheme.rawValue)://" } } - + if viewModel.isLoading { Button(role: .destructive) { viewModel.cancelConnection() @@ -47,7 +47,7 @@ struct ConnectToServerView: View { } header: { Text("Connect to a Jellyfin server") } - + Section { if viewModel.searching { HStack(alignment: .center, spacing: 5) { @@ -90,7 +90,7 @@ struct ConnectToServerView: View { HStack { L10n.localServers.text Spacer() - + Button { viewModel.discoverServers() } label: { diff --git a/JellyfinPlayer/Views/HomeView.swift b/JellyfinPlayer/Views/HomeView.swift index f5fb2da5..26132fb9 100644 --- a/JellyfinPlayer/Views/HomeView.swift +++ b/JellyfinPlayer/Views/HomeView.swift @@ -12,10 +12,10 @@ import Introspect import SwiftUI struct HomeView: View { - + @EnvironmentObject var homeRouter: HomeCoordinator.Router @StateObject var viewModel = HomeViewModel() - + private let refreshHelper = RefreshHelper() @ViewBuilder @@ -31,7 +31,7 @@ struct HomeView: View { if !viewModel.nextUpItems.isEmpty { NextUpView(items: viewModel.nextUpItems) } - + ForEach(viewModel.libraries, id: \.self) { library in HStack { Text(L10n.latestWithString(library.name ?? "")) @@ -58,10 +58,10 @@ struct HomeView: View { } .introspectScrollView { scrollView in let control = UIRefreshControl() - + refreshHelper.refreshControl = control refreshHelper.refreshAction = viewModel.refresh - + control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged) scrollView.refreshControl = control } diff --git a/JellyfinPlayer/Views/ItemView/ItemView.swift b/JellyfinPlayer/Views/ItemView/ItemView.swift index de799478..95d37413 100644 --- a/JellyfinPlayer/Views/ItemView/ItemView.swift +++ b/JellyfinPlayer/Views/ItemView/ItemView.swift @@ -28,7 +28,7 @@ struct ItemNavigationView: View { } } -fileprivate struct ItemView: View { +private struct ItemView: View { @EnvironmentObject var itemRouter: ItemCoordinator.Router @State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view. diff --git a/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift index 24a9e15a..c0626b1c 100644 --- a/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift +++ b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeTopBarView.swift @@ -10,22 +10,22 @@ import SwiftUI struct ItemLandscapeTopBarView: View { - + @EnvironmentObject private var viewModel: ItemViewModel - + var body: some View { HStack { VStack(alignment: .leading) { - + // MARK: Name - + Text(viewModel.getItemDisplayName()) .font(.title) .fontWeight(.semibold) .foregroundColor(.primary) .padding(.leading, 16) .padding(.bottom, 10) - + if viewModel.item.itemType.showDetails { // MARK: Runtime Text(viewModel.item.getItemRuntime()) @@ -34,7 +34,7 @@ struct ItemLandscapeTopBarView: View { .foregroundColor(.secondary) .padding(.leading, 16) } - + // MARK: Details HStack { if viewModel.item.productionYear != nil { @@ -53,9 +53,9 @@ struct ItemLandscapeTopBarView: View { .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) } - + Spacer() - + if viewModel.item.itemType.showDetails { // MARK: Favorite Button { @@ -70,7 +70,7 @@ struct ItemLandscapeTopBarView: View { } } .disabled(viewModel.isLoading) - + // MARK: Watched Button { viewModel.updateWatchState() diff --git a/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift index cb7c28a5..99a069d5 100644 --- a/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift +++ b/JellyfinPlayer/Views/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift @@ -11,22 +11,22 @@ import SwiftUI import JellyfinAPI struct PortraitHeaderOverlayView: View { - + @EnvironmentObject private var viewModel: ItemViewModel @EnvironmentObject private var videoPlayerItem: VideoPlayerItem - + var body: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - + // MARK: Portrait Image ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130)) .frame(width: 130, height: 195) .cornerRadius(10) - + VStack(alignment: .leading, spacing: 1) { Spacer() - + // MARK: Name Text(viewModel.getItemDisplayName()) .font(.title2) @@ -34,7 +34,7 @@ struct PortraitHeaderOverlayView: View { .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 10) - + if viewModel.item.itemType.showDetails { // MARK: Runtime if viewModel.shouldDisplayRuntime() { @@ -45,7 +45,7 @@ struct PortraitHeaderOverlayView: View { .lineLimit(1) } } - + // MARK: Details HStack { if let productionYear = viewModel.item.productionYear { @@ -55,7 +55,7 @@ struct PortraitHeaderOverlayView: View { .foregroundColor(.secondary) .lineLimit(1) } - + if let officialRating = viewModel.item.officialRating { Text(officialRating) .font(.subheadline) @@ -70,9 +70,9 @@ struct PortraitHeaderOverlayView: View { } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) } - + HStack { - + // MARK: Play Button { if let playButtonItem = viewModel.playButtonItem { @@ -93,9 +93,9 @@ struct PortraitHeaderOverlayView: View { .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) .cornerRadius(10) }.disabled(viewModel.playButtonItem == nil) - + Spacer() - + if viewModel.item.itemType.showDetails { // MARK: Favorite Button { @@ -112,7 +112,7 @@ struct PortraitHeaderOverlayView: View { } } .disabled(viewModel.isLoading) - + // MARK: Watched Button { viewModel.updateWatchState() diff --git a/JellyfinPlayer/Views/LibraryFilterView.swift b/JellyfinPlayer/Views/LibraryFilterView.swift index 532b9e95..5864a6b1 100644 --- a/JellyfinPlayer/Views/LibraryFilterView.swift +++ b/JellyfinPlayer/Views/LibraryFilterView.swift @@ -10,7 +10,7 @@ import Stinsen import SwiftUI struct LibraryFilterView: View { - + @EnvironmentObject var filterRouter: FilterCoordinator.Router @Binding var filters: LibraryFilters var parentId: String = "" diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift index 07fa6af9..7ab2709c 100644 --- a/JellyfinPlayer/Views/ServerListView.swift +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -11,10 +11,10 @@ import CoreStore import SwiftUI struct ServerListView: View { - + @EnvironmentObject var serverListRouter: ServerListCoordinator.Router @ObservedObject var viewModel: ServerListViewModel - + private var listView: some View { ScrollView { LazyVStack { @@ -27,22 +27,22 @@ struct ServerListView: View { .foregroundColor(Color(UIColor.secondarySystemFill)) .frame(height: 100) .cornerRadius(10) - + HStack(spacing: 10) { Image(systemName: "server.rack") .font(.system(size: 36)) .foregroundColor(.primary) - + VStack(alignment: .leading, spacing: 5) { Text(server.name) .font(.title2) .foregroundColor(.primary) - + Text(server.uri) .font(.footnote) .disabled(true) .foregroundColor(.secondary) - + Text(viewModel.userTextFor(server: server)) .font(.footnote) .foregroundColor(.primary) @@ -62,13 +62,13 @@ struct ServerListView: View { } } } - + private var noServerView: some View { VStack { Text("Connect to a Jellyfin server to get started") .frame(minWidth: 50, maxWidth: 240) .multilineTextAlignment(.center) - + Button { serverListRouter.route(to: \.connectToServer) } label: { @@ -80,7 +80,7 @@ struct ServerListView: View { .cornerRadius(10) .padding(.horizontal, 30) .padding([.top, .bottom], 20) - + L10n.connect.text .foregroundColor(Color.white) .bold() @@ -88,7 +88,7 @@ struct ServerListView: View { } } } - + @ViewBuilder private var innerBody: some View { if viewModel.servers.isEmpty { @@ -98,7 +98,7 @@ struct ServerListView: View { listView } } - + @ViewBuilder private var trailingToolbarContent: some View { if viewModel.servers.isEmpty { @@ -111,7 +111,7 @@ struct ServerListView: View { } } } - + private var leadingToolbarContent: some View { Button { serverListRouter.route(to: \.basicAppSettings) @@ -119,7 +119,7 @@ struct ServerListView: View { Image(systemName: "gearshape.fill") } } - + var body: some View { innerBody .navigationTitle("Servers") diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index ef9a0916..ef21dc0f 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct SettingsView: View { - + @EnvironmentObject var settingsRouter: SettingsCoordinator.Router @ObservedObject var viewModel: SettingsViewModel @@ -27,7 +27,7 @@ struct SettingsView: View { var body: some View { Form { Section(header: EmptyView()) { - + // There is a bug where the SettingsView attmempts to remake itself upon signing out // so this check is made if SessionManager.main.currentLogin == nil { @@ -81,7 +81,7 @@ struct SettingsView: View { .font(.callout) } } - + Section(header: Text("Playback")) { Picker("Default local quality", selection: $inNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in diff --git a/JellyfinPlayer/Views/UserListView.swift b/JellyfinPlayer/Views/UserListView.swift index cd2f6411..382b53d5 100644 --- a/JellyfinPlayer/Views/UserListView.swift +++ b/JellyfinPlayer/Views/UserListView.swift @@ -10,10 +10,10 @@ import SwiftUI struct UserListView: View { - + @EnvironmentObject var userListRouter: UserListCoordinator.Router @ObservedObject var viewModel: UserListViewModel - + private var listView: some View { ScrollView { LazyVStack { @@ -26,13 +26,13 @@ struct UserListView: View { .foregroundColor(Color(UIColor.secondarySystemFill)) .frame(height: 50) .cornerRadius(10) - + HStack { Text(user.username) .font(.title2) - + Spacer() - + if viewModel.isLoading { ProgressView() } @@ -51,13 +51,13 @@ struct UserListView: View { } } } - + private var noUserView: some View { VStack { Text("Sign in to get started") .frame(minWidth: 50, maxWidth: 240) .multilineTextAlignment(.center) - + Button { userListRouter.route(to: \.userSignIn, viewModel.server) } label: { @@ -69,7 +69,7 @@ struct UserListView: View { .cornerRadius(10) .padding(.horizontal, 30) .padding([.top, .bottom], 20) - + Text("Sign in") .foregroundColor(Color.white) .bold() @@ -77,7 +77,7 @@ struct UserListView: View { } } } - + @ViewBuilder private var innerBody: some View { if viewModel.users.isEmpty { @@ -87,7 +87,7 @@ struct UserListView: View { listView } } - + @ViewBuilder private var toolbarContent: some View { if viewModel.users.isEmpty { @@ -102,7 +102,7 @@ struct UserListView: View { } } } - + var body: some View { innerBody .navigationTitle(viewModel.server.name) diff --git a/JellyfinPlayer/Views/UserSignInView.swift b/JellyfinPlayer/Views/UserSignInView.swift index 84544533..72a1390f 100644 --- a/JellyfinPlayer/Views/UserSignInView.swift +++ b/JellyfinPlayer/Views/UserSignInView.swift @@ -11,23 +11,23 @@ import SwiftUI import Stinsen struct UserSignInView: View { - + @ObservedObject var viewModel: UserSignInViewModel @State private var username: String = "" @State private var password: String = "" - + var body: some View { Form { - + Section { TextField(L10n.username, text: $username) .disableAutocorrection(true) .autocapitalization(.none) - + SecureField(L10n.password, text: $password) .disableAutocorrection(true) .autocapitalization(.none) - + if viewModel.isLoading { Button(role: .destructive) { viewModel.cancelSignIn() diff --git a/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift index e5c36f61..fc46d2d2 100644 --- a/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VideoPlayer.swift @@ -153,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe sendProgressReport(eventName: "unpause") } else { sendJellyfinCommand(command: "Seek", options: [ - "position": Int(secondsScrubbedTo), + "position": Int(secondsScrubbedTo) ]) } } @@ -664,8 +664,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { if Defaults[.autoSelectSubtitlesLangCode] == "Auto", - subtitle.languageCode.contains(Locale.current.languageCode ?? "") - { + subtitle.languageCode.contains(Locale.current.languageCode ?? "") { selectedCaptionTrack = subtitle.id mediaPlayer.currentVideoSubTitleIndex = subtitle.id } else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { @@ -854,7 +853,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { if hasSentRemoteSeek == false { hasSentRemoteSeek = true 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, "serverVersion": "10.8.0", "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, - "subtitleBurnIn": false, + "subtitleBurnIn": false ] let jsonData = JSON(payload) @@ -935,8 +934,8 @@ extension PlayerViewController: GCKSessionManagerListener { "Name": manifest.name!, "Type": manifest.type!, "MediaType": manifest.mediaType!, - "IsFolder": manifest.isFolder!, - ]], + "IsFolder": manifest.isFolder! + ]] ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } @@ -1104,8 +1103,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { typealias UIViewControllerType = PlayerViewController func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls - .UIViewControllerType - { + .UIViewControllerType { let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController customViewController.manifest = item diff --git a/Shared/Coordinators/BasicAppSettingsCoordinator.swift b/Shared/Coordinators/BasicAppSettingsCoordinator.swift index d9ee38bb..f9b0d5f5 100644 --- a/Shared/Coordinators/BasicAppSettingsCoordinator.swift +++ b/Shared/Coordinators/BasicAppSettingsCoordinator.swift @@ -12,11 +12,11 @@ import Stinsen import SwiftUI final class BasicAppSettingsCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start) - + @Root var start = makeStart - + @ViewBuilder func makeStart() -> some View { BasicAppSettingsView(viewModel: BasicAppSettingsViewModel()) } diff --git a/Shared/Coordinators/ConnectToServerCoodinator.swift b/Shared/Coordinators/ConnectToServerCoodinator.swift index 45d47f03..ef2321f7 100644 --- a/Shared/Coordinators/ConnectToServerCoodinator.swift +++ b/Shared/Coordinators/ConnectToServerCoodinator.swift @@ -12,16 +12,16 @@ import Stinsen import SwiftUI final class ConnectToServerCoodinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \ConnectToServerCoodinator.start) @Root var start = makeStart @Route(.push) var userSignIn = makeUserSignIn - + func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { return UserSignInCoordinator(viewModel: .init(server: server)) } - + @ViewBuilder func makeStart() -> some View { ConnectToServerView(viewModel: ConnectToServerViewModel()) } diff --git a/Shared/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift index fa845d0c..7ff6b7c5 100644 --- a/Shared/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -14,9 +14,9 @@ import SwiftUI typealias FilterCoordinatorParams = (filters: Binding, enabledFilterType: [FilterType], parentId: String) final class FilterCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \FilterCoordinator.start) - + @Root var start = makeStart @Binding var filters: LibraryFilters diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index e30c79af..e373de0b 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -13,7 +13,7 @@ import Stinsen import SwiftUI final class HomeCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \HomeCoordinator.start) @Root var start = makeStart @@ -34,11 +34,11 @@ final class HomeCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } - + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { return NavigationViewCoordinator(ItemCoordinator(item: item)) } - + func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) } diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index 5e578efb..286c2bd2 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -13,7 +13,7 @@ import Stinsen import SwiftUI final class ItemCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \ItemCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index a5ef3495..50a4485a 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -15,7 +15,7 @@ import SwiftUI typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String) final class LibraryCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \LibraryCoordinator.start) @Root var start = makeStart @@ -49,7 +49,7 @@ final class LibraryCoordinator: NavigationCoordinatable { func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } - + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { return NavigationViewCoordinator(ItemCoordinator(item: item)) } diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift index 88377644..e79f25c2 100644 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -12,7 +12,7 @@ import Stinsen import SwiftUI final class LibraryListCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \LibraryListCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index 9c82fd44..5bb52694 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -25,13 +25,13 @@ final class MainCoordinator: NavigationCoordinatable { } else { self.stack = NavigationStack(initial: \MainCoordinator.serverList) } - + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk WidgetCenter.shared.reloadAllTimelines() UIScrollView.appearance().keyboardDismissMode = .onDrag - + // Back bar button item setup let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") let barAppearance = UINavigationBar.appearance() diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index 1c16c485..cfa95e16 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -42,7 +42,7 @@ final class MainTabCoordinator: TabCoordinatable { view.onAppear { AppURLHandler.shared.appURLState = .allowed // TODO: todo - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { AppURLHandler.shared.processLaunchedURLIfNeeded() } } diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift index 05d77b94..122a870f 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift @@ -17,14 +17,14 @@ final class MainCoordinator: NavigationCoordinatable { @Root var mainTab = makeMainTab @Root var serverList = makeServerList - + init() { if SessionManager.main.currentLogin != nil { self.stack = NavigationStack(initial: \MainCoordinator.mainTab) } else { self.stack = NavigationStack(initial: \MainCoordinator.serverList) } - + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 3f3877d0..ea0a5be7 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -19,24 +19,24 @@ final class MainTabCoordinator: TabCoordinatable { \MainTabCoordinator.other, \MainTabCoordinator.settings ]) - + @Route(tabItem: makeHomeTab) var home = makeHome @Route(tabItem: makeTvTab) var tv = makeTv @Route(tabItem: makeMoviesTab) var movies = makeMovies @Route(tabItem: makeOtherTab) var other = makeOther @Route(tabItem: makeSettingsTab) var settings = makeSettings - + func makeHome() -> NavigationViewCoordinator { return NavigationViewCoordinator(HomeCoordinator()) } - + @ViewBuilder func makeHomeTab(isActive: Bool) -> some View { HStack { Image(systemName: "house") L10n.home.text } } - + func makeTv() -> NavigationViewCoordinator { return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows")) } @@ -47,7 +47,7 @@ final class MainTabCoordinator: TabCoordinatable { Text("TV Shows") } } - + func makeMovies() -> NavigationViewCoordinator { return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies")) } @@ -69,11 +69,11 @@ final class MainTabCoordinator: TabCoordinatable { Text("Other") } } - + func makeSettings() -> NavigationViewCoordinator { return NavigationViewCoordinator(SettingsCoordinator()) } - + @ViewBuilder func makeSettingsTab(isActive: Bool) -> some View { HStack { Image(systemName: "gearshape.fill") diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift index 9c530d3c..9adda2e5 100644 --- a/Shared/Coordinators/MoviesLibrariesCoordinator.swift +++ b/Shared/Coordinators/MoviesLibrariesCoordinator.swift @@ -13,7 +13,7 @@ import Stinsen import SwiftUI final class MovieLibrariesCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index 9d66e6b4..40388163 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -13,7 +13,7 @@ import SwiftUI import JellyfinAPI final class SearchCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \SearchCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/ServerListCoordinator.swift b/Shared/Coordinators/ServerListCoordinator.swift index d60abebd..ba773b7b 100644 --- a/Shared/Coordinators/ServerListCoordinator.swift +++ b/Shared/Coordinators/ServerListCoordinator.swift @@ -12,26 +12,26 @@ import Stinsen import SwiftUI final class ServerListCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \ServerListCoordinator.start) - + @Root var start = makeStart @Route(.push) var connectToServer = makeConnectToServer @Route(.push) var userList = makeUserList @Route(.modal) var basicAppSettings = makeBasicAppSettings - + func makeConnectToServer() -> ConnectToServerCoodinator { ConnectToServerCoodinator() } - + func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator { UserListCoordinator(viewModel: .init(server: server)) } - + func makeBasicAppSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(BasicAppSettingsCoordinator()) } - + @ViewBuilder func makeStart() -> some View { ServerListView(viewModel: ServerListViewModel()) } diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 0b8f8a23..9e774e7c 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -12,7 +12,7 @@ import Stinsen import SwiftUI final class SettingsCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \SettingsCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift index 2ad50744..b3f85337 100644 --- a/Shared/Coordinators/TVLibrariesCoordinator.swift +++ b/Shared/Coordinators/TVLibrariesCoordinator.swift @@ -13,7 +13,7 @@ import Stinsen import SwiftUI final class TVLibrariesCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \TVLibrariesCoordinator.start) @Root var start = makeStart diff --git a/Shared/Coordinators/UserListCoordinator.swift b/Shared/Coordinators/UserListCoordinator.swift index ff728bb6..44695c0d 100644 --- a/Shared/Coordinators/UserListCoordinator.swift +++ b/Shared/Coordinators/UserListCoordinator.swift @@ -12,22 +12,22 @@ import Stinsen import SwiftUI final class UserListCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \UserListCoordinator.start) - + @Root var start = makeStart @Route(.push) var userSignIn = makeUserSignIn - + let viewModel: UserListViewModel - + init(viewModel: UserListViewModel) { self.viewModel = viewModel } - + func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { return UserSignInCoordinator(viewModel: .init(server: server)) } - + @ViewBuilder func makeStart() -> some View { UserListView(viewModel: viewModel) } diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift index f4e03a87..09d45e1e 100644 --- a/Shared/Coordinators/UserSignInCoordinator.swift +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -12,17 +12,17 @@ import Stinsen import SwiftUI final class UserSignInCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \UserSignInCoordinator.start) - + @Root var start = makeStart - + let viewModel: UserSignInViewModel - + init(viewModel: UserSignInViewModel) { self.viewModel = viewModel } - + @ViewBuilder func makeStart() -> some View { UserSignInView(viewModel: viewModel) } diff --git a/Shared/Coordinators/VideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator.swift index 921d52f9..b4feb6fd 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator.swift @@ -13,11 +13,11 @@ import Stinsen import SwiftUI final class VideoPlayerCoordinator: NavigationCoordinatable { - + let stack = NavigationStack(initial: \VideoPlayerCoordinator.start) @Root var start = makeStart - + let item: BaseItemDto init(item: BaseItemDto) { diff --git a/Shared/Errors/ErrorMessage.swift b/Shared/Errors/ErrorMessage.swift index 0f14fe59..6b2ddb8b 100644 --- a/Shared/Errors/ErrorMessage.swift +++ b/Shared/Errors/ErrorMessage.swift @@ -16,7 +16,7 @@ struct ErrorMessage: Identifiable { let title: String let displayMessage: String let logConstructor: LogConstructor - + // Chosen value such that if an error has this code, don't show the code to the UI // This was chosen because of its unlikelyhood to ever be used static let noShowErrorCode = -69420 diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift index 6ca62ffd..b4b23403 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Stackable.swift @@ -15,11 +15,11 @@ extension BaseItemDto: PortraitImageStackable { public func imageURLContsructor(maxWidth: Int) -> URL { return self.getPrimaryImage(maxWidth: maxWidth) } - + public var title: String { return self.name ?? "" } - + public var description: String? { switch self.itemType { case .season: @@ -31,11 +31,11 @@ extension BaseItemDto: PortraitImageStackable { return nil } } - + public var blurHash: String { return self.getPrimaryImageBlurHash() } - + public var failureInitials: String { guard let name = self.name else { return "" } let initials = name.split(separator: " ").compactMap({ String($0).first }) diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index da06e0ba..95036a2f 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -14,7 +14,7 @@ import UIKit // 001fC^ = dark grey plain blurhash public extension BaseItemDto { - + // MARK: Images func getSeriesBackdropImageBlurHash() -> String { @@ -152,17 +152,17 @@ public extension BaseItemDto { return "\(String(progminutes))m" } } - + // MARK: ItemType - + enum ItemType: String { case movie = "Movie" case season = "Season" case episode = "Episode" case series = "Series" - + case unknown - + var showDetails: Bool { switch self { case .season, .series: @@ -172,14 +172,14 @@ public extension BaseItemDto { } } } - + var itemType: ItemType { guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown } return knownType } - + // MARK: PortraitHeaderViewURL - + func portraitHeaderViewURL(maxWidth: Int) -> URL { switch self.itemType { case .movie, .season, .series: diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index 63b2d239..007ed999 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -10,7 +10,7 @@ import JellyfinAPI import UIKit extension BaseItemPerson { - + // MARK: Get Image func getImage(baseURL: String, maxWidth: Int) -> URL { let imageType = "Primary" @@ -28,10 +28,9 @@ extension BaseItemPerson { return imageBlurHashes?.primary?[imgTag] ?? "001fC^" } - - + // MARK: First 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: // - assumes that the most important role is the first @@ -40,16 +39,16 @@ extension BaseItemPerson { guard let role = self.role else { return nil } let split = role.split(separator: "/") 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 } - + var final = firstRole - + if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") { let roleText = lastRole[lastOpenIndex...lastClosingIndex] final.append(" \(roleText)") } - + return final } } @@ -59,19 +58,19 @@ extension BaseItemPerson: PortraitImageStackable { public func imageURLContsructor(maxWidth: Int) -> URL { return self.getImage(baseURL: SessionManager.main.currentLogin.server.uri, maxWidth: maxWidth) } - + public var title: String { return self.name ?? "" } - + public var description: String? { return self.firstRole() } - + public var blurHash: String { return self.getBlurHash() } - + public var failureInitials: String { guard let name = self.name else { return "" } let initials = name.split(separator: " ").compactMap({ String($0).first }) @@ -81,7 +80,7 @@ extension BaseItemPerson: PortraitImageStackable { // MARK: DiplayedType extension BaseItemPerson { - + // Only displayed person types. // Will ignore people like "GuestStar" enum DisplayedType: String, CaseIterable { @@ -89,7 +88,7 @@ extension BaseItemPerson { case director = "Director" case writer = "Writer" case producer = "Producer" - + static var allCasesRaw: [String] { return self.allCases.map({ $0.rawValue }) } diff --git a/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift index f74a5a3c..0bfe3a81 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/JellyfinAPIError.swift @@ -10,13 +10,13 @@ import Foundation struct JellyfinAPIError: Error { - + private let message: String - + init(_ message: String) { self.message = message } - + var localizedDescription: String { return message } diff --git a/Shared/Extensions/StringExtensions.swift b/Shared/Extensions/StringExtensions.swift index b442b0b5..64b4a63d 100644 --- a/Shared/Extensions/StringExtensions.swift +++ b/Shared/Extensions/StringExtensions.swift @@ -32,7 +32,7 @@ extension String { return "\(padString)\(self)" } - + var text: Text { Text(self) } diff --git a/Shared/Objects/DetailItem.swift b/Shared/Objects/DetailItem.swift index bf5b1792..fd017f2d 100644 --- a/Shared/Objects/DetailItem.swift +++ b/Shared/Objects/DetailItem.swift @@ -18,11 +18,8 @@ enum DetailItemType: String { } struct DetailItem { - + let baseItem: BaseItemDto let type: DetailItemType - - - - + } diff --git a/Shared/Objects/Typings.swift b/Shared/Objects/Typings.swift index 60d7a646..64293d42 100644 --- a/Shared/Objects/Typings.swift +++ b/Shared/Objects/Typings.swift @@ -73,7 +73,7 @@ enum ItemType: String { case movie = "Movie" case series = "Series" case season = "Season" - + var localized: String { switch self { case .episode: diff --git a/Shared/ServerDiscovery/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift index 6f34789d..841b8889 100644 --- a/Shared/ServerDiscovery/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -46,7 +46,7 @@ public class ServerDiscovery { case name = "Name" } } - + private let broadcastConn: UDPBroadcastConnection public init() { diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index d5fdd1c0..d95db42f 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -19,83 +19,83 @@ typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStor // MARK: NewSessionManager final class SessionManager { - + // MARK: currentLogin private(set) var currentLogin: CurrentLogin! - + // MARK: main static let main = SessionManager() - + private init() { if let lastUserID = SwiftfinStore.Defaults.suite[.lastServerUserID], let user = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", lastUserID)]) { - + 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 } - + JellyfinAPI.basePath = server.uri setAuthHeader(with: accessToken.value) currentLogin = (server: existingServer.state, user: user.state) } } - + private func generateServerUserID(server: SwiftfinStore.Models.StoredServer, user: SwiftfinStore.Models.StoredUser) -> String { return "\(server.id)-\(user.id)" } - + func fetchServers() -> [SwiftfinStore.State.Server] { let servers = try! SwiftfinStore.dataStack.fetchAll(From()) return servers.map({ $0.state }) } - + func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] { guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From(), Where("id == %@", server.id)) else { fatalError("No stored server associated with given state server?") } return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username }) } - + // Connects to a server at the given uri, storing if successful func connectToServer(with uri: String) -> AnyPublisher { var uriComponents = URLComponents(string: uri) ?? URLComponents() - + if uriComponents.scheme == nil { uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue } - + var uri = uriComponents.string ?? "" - + if uri.last == "/" { uri = String(uri.dropLast()) } - + JellyfinAPI.basePath = uri - + return SystemAPI.getPublicSystemInfo() .tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in - + let transaction = SwiftfinStore.dataStack.beginUnsafe() let newServer = transaction.create(Into()) - + guard let name = response.serverName, let id = response.id, let os = response.operatingSystem, let version = response.version else { throw JellyfinAPIError("Missing server data from network call") } - + newServer.uri = uri newServer.name = name newServer.id = id newServer.os = os newServer.version = version newServer.users = [] - + // Check for existing server on device if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", newServer.id)]) { throw SwiftfinStore.Errors.existingServer(existingServer.state) } - + return (newServer, transaction) }) .handleEvents(receiveOutput: { (_, transaction) in @@ -106,57 +106,57 @@ final class SessionManager { }) .eraseToAnyPublisher() } - + // Logs in a user with an associated server, storing if successful func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher { setAuthHeader(with: "") - + JellyfinAPI.basePath = server.uri - + return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) .tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in - + guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") } - + let transaction = SwiftfinStore.dataStack.beginUnsafe() let newUser = transaction.create(Into()) - + guard let username = response.user?.name, let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") } - + newUser.username = username newUser.id = id newUser.appleTVID = "" - + // Check for existing user on device if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", newUser.id)]) { throw SwiftfinStore.Errors.existingUser(existingUser.state) } - + let newAccessToken = transaction.create(Into()) newAccessToken.value = accessToken newUser.accessToken = newAccessToken - + guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", server.id)]) 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?") } editUserServer.users.insert(newUser) - + return (editUserServer, newUser, transaction) }) .handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in setAuthHeader(with: user.accessToken?.value ?? "") try? transaction.commitAndWait() - + // Fetch for the right queue let currentServer = SwiftfinStore.dataStack.fetchExisting(server)! let currentUser = SwiftfinStore.dataStack.fetchExisting(user)! - + SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id - + currentLogin = (server: currentServer.state, user: currentUser.state) SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) }) @@ -165,7 +165,7 @@ final class SessionManager { }) .eraseToAnyPublisher() } - + func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { JellyfinAPI.basePath = server.uri SwiftfinStore.Defaults.suite[.lastServerUserID] = user.id @@ -173,7 +173,7 @@ final class SessionManager { currentLogin = (server: server, user: user) SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) } - + func logout() { currentLogin = nil JellyfinAPI.basePath = "" @@ -181,66 +181,66 @@ final class SessionManager { SwiftfinStore.Defaults.suite[.lastServerUserID] = nil SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) } - + func purge() { // Delete all servers let servers = fetchServers() - + for server in servers { delete(server: server) } - + // Delete UserDefaults SwiftfinStore.Defaults.suite.removeAll() - + SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) } - + func delete(user: SwiftfinStore.State.User) { guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", user.id)]) else { fatalError("No stored user for state user?")} _delete(user: storedUser, transaction: nil) } - + func delete(server: SwiftfinStore.State.Server) { guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From(), [Where("id == %@", server.id)]) else { fatalError("No stored server for state server?")} _delete(server: storedServer, transaction: nil) } - + private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) { guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")} - + let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! transaction.delete(storedAccessToken) transaction.delete(user) try? transaction.commitAndWait() } - + private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) { let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! - + for user in server.users { _delete(user: user, transaction: transaction) } - + transaction.delete(server) try? transaction.commitAndWait() } - + private func setAuthHeader(with accessToken: String) { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String var deviceName = UIDevice.current.name deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) }) - + let platform: String #if os(tvOS) platform = "tvOS" #else platform = "iOS" #endif - + var header = "MediaBrowser " header.append("Client=\"Jellyfin \(platform)\", ") header.append("Device=\"\(deviceName)\", ") diff --git a/Shared/Singleton/SwiftfinNotificationCenter.swift b/Shared/Singleton/SwiftfinNotificationCenter.swift index c20973a2..7a9c2bd4 100644 --- a/Shared/Singleton/SwiftfinNotificationCenter.swift +++ b/Shared/Singleton/SwiftfinNotificationCenter.swift @@ -10,11 +10,11 @@ import Foundation enum SwiftfinNotificationCenter { - + static let main: NotificationCenter = { return NotificationCenter() }() - + enum Keys { static let didSignIn = Notification.Name("didSignIn") static let didSignOut = Notification.Name("didSignOut") diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index 1711e9cb..2b6cfc08 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -12,12 +12,12 @@ import CoreStore import Defaults enum SwiftfinStore { - + // MARK: State // Safe, copyable representations of their underlying CoreStoredObject's // Relationships are represented by the related object's IDs or value enum State { - + struct Server { let uri: String let name: String @@ -25,7 +25,7 @@ enum SwiftfinStore { let os: String let version: String let userIDs: [String] - + fileprivate init(uri: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { self.uri = uri self.name = name @@ -34,54 +34,54 @@ enum SwiftfinStore { self.version = version self.userIDs = usersIDs } - + 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"]) } } - + struct User { let username: String let id: String let serverID: String let accessToken: String - + fileprivate init(username: String, id: String, serverID: String, accessToken: String) { self.username = username self.id = id self.serverID = serverID self.accessToken = accessToken } - + static var sample: User { return User(username: "JohnnyAppleseed", id: "123abc", serverID: "123abc", accessToken: "open-sesame") } } } - + // MARK: Models enum Models { - + final class StoredServer: CoreStoreObject { - + @Field.Stored("uri") var uri: String = "" - + @Field.Stored("name") var name: String = "" - + @Field.Stored("id") var id: String = "" - + @Field.Stored("os") var os: String = "" - + @Field.Stored("version") var version: String = "" - + @Field.Relationship("users", inverse: \StoredUser.$server) var users: Set - + var state: State.Server { return State.Server(uri: uri, name: name, @@ -91,24 +91,24 @@ enum SwiftfinStore { usersIDs: users.map({ $0.id })) } } - + final class StoredUser: CoreStoreObject { - + @Field.Stored("username") var username: String = "" - + @Field.Stored("id") var id: String = "" - + @Field.Stored("appleTVID") var appleTVID: String = "" - + @Field.Relationship("server") var server: StoredServer? - + @Field.Relationship("accessToken", inverse: \StoredAccessToken.$user) var accessToken: StoredAccessToken? - + var state: State.User { guard let server = server else { fatalError("No server associated with user") } guard let accessToken = accessToken else { fatalError("No access token associated with user") } @@ -118,23 +118,23 @@ enum SwiftfinStore { accessToken: accessToken.value) } } - + final class StoredAccessToken: CoreStoreObject { - + @Field.Stored("value") var value: String = "" - + @Field.Relationship("user") var user: StoredUser? } } - + // MARK: Errors enum Errors { case existingServer(State.Server) case existingUser(State.User) } - + // MARK: dataStack static let dataStack: DataStack = { let schema = CoreStoreSchema(modelVersion: "V1", @@ -148,7 +148,7 @@ enum SwiftfinStore { "Server": [0x39c64a826739077e, 0xa7ac63744fd7df32, 0xef3c9d4fe638fbfb, 0xdabd796256df14db], "User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] ]) - + let _dataStack = DataStack(schema) try! _dataStack.addStorageAndWait( SQLiteStore( @@ -162,7 +162,7 @@ enum SwiftfinStore { // MARK: LocalizedError extension SwiftfinStore.Errors: LocalizedError { - + var title: String { switch self { case .existingServer(_): @@ -171,7 +171,7 @@ extension SwiftfinStore.Errors: LocalizedError { return "Existing User" } } - + var errorDescription: String? { switch self { case .existingServer(let server): diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index ab6a28d0..22146519 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -11,9 +11,9 @@ import Defaults import Foundation extension SwiftfinStore { - + enum Defaults { - + static let suite: UserDefaults = { return UserDefaults(suiteName: "swiftfinstore-defaults")! }() @@ -22,7 +22,7 @@ extension SwiftfinStore { extension Defaults.Keys { static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.suite) - + static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite) static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite) diff --git a/Shared/ViewModels/BasicAppSettingsViewModel.swift b/Shared/ViewModels/BasicAppSettingsViewModel.swift index ca3d477e..9cc1fa43 100644 --- a/Shared/ViewModels/BasicAppSettingsViewModel.swift +++ b/Shared/ViewModels/BasicAppSettingsViewModel.swift @@ -10,9 +10,9 @@ import SwiftUI final class BasicAppSettingsViewModel: ViewModel { - + let appearances = AppAppearance.allCases - + func reset() { SessionManager.main.purge() } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 2f4b1460..c42c1e44 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -13,12 +13,12 @@ import JellyfinAPI import Stinsen final class ConnectToServerViewModel: ViewModel { - + @RouterObject var router: ConnectToServerCoodinator.Router? @Published var discoveredServers: Set = [] @Published var searching = false private let discovery = ServerDiscovery() - + var alertTitle: String { var message: String = "" if errorMessage?.code != ErrorMessage.noShowErrorCode { @@ -64,12 +64,12 @@ final class ConnectToServerViewModel: ViewModel { } } } - + func cancelConnection() { for cancellable in cancellables { cancellable.cancel() } - + self.isLoading = false } } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index bd2c1e24..3e70f571 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -39,9 +39,9 @@ final class HomeViewModel: ViewModel { self.handleAPIRequestError(completion: completion) } }, receiveValue: { response in - + var newLibraries: [BaseItemDto] = [] - + response.items!.forEach { item in LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") if item.collectionType == "movies" || item.collectionType == "tvshows" { @@ -60,13 +60,13 @@ final class HomeViewModel: ViewModel { } }, receiveValue: { response in let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : [] - + for excludeID in excludeIDs { newLibraries.removeAll { library in return library.id == excludeID } } - + self.libraries = newLibraries }) .store(in: &self.cancellables) @@ -88,7 +88,7 @@ final class HomeViewModel: ViewModel { } }, receiveValue: { response in LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") - + self.resumeItems = response.items ?? [] }) .store(in: &cancellables) @@ -105,7 +105,7 @@ final class HomeViewModel: ViewModel { } }, receiveValue: { response in LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") - + self.nextUpItems = response.items ?? [] }) .store(in: &cancellables) diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel.swift index 473f8436..adb14d20 100644 --- a/Shared/ViewModels/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel.swift @@ -11,7 +11,7 @@ import Foundation import JellyfinAPI class ItemViewModel: ViewModel { - + @Published var item: BaseItemDto @Published var playButtonItem: BaseItemDto? @Published var similarItems: [BaseItemDto] = [] @@ -20,32 +20,32 @@ class ItemViewModel: ViewModel { init(item: BaseItemDto) { self.item = item - + switch item.itemType { case .episode, .movie: self.playButtonItem = item default: () } - + isFavorited = item.userData?.isFavorite ?? false isWatched = item.userData?.played ?? false super.init() getSimilarItems() } - + func playButtonText() -> String { return item.getItemProgressString() == "" ? L10n.play : item.getItemProgressString() } - + func getItemDisplayName() -> String { return item.name ?? "" } - + func shouldDisplayRuntime() -> Bool { return true } - + func getSimilarItems() { LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) .trackActivity(loading) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index f0839708..ae134564 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -36,7 +36,7 @@ final class LibraryViewModel: ViewModel { // temp @Published var filters: LibraryFilters - + private let columns: Int private var libraries = [BaseItemDto]() @@ -64,11 +64,10 @@ final class LibraryViewModel: ViewModel { self.columns = columns super.init() - $filters .sink(receiveValue: requestItems(with:)) .store(in: &cancellables) - + } func requestItems(with filters: LibraryFilters) { @@ -147,7 +146,7 @@ final class LibraryViewModel: ViewModel { currentPage -= 1 requestItems(with: filters) } - + private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] { guard itemList.count > 0 else { return [] } let rowCount = itemList.count / columns diff --git a/Shared/ViewModels/MovieLibrariesViewModel.swift b/Shared/ViewModels/MovieLibrariesViewModel.swift index 17bd4567..80bb2838 100644 --- a/Shared/ViewModels/MovieLibrariesViewModel.swift +++ b/Shared/ViewModels/MovieLibrariesViewModel.swift @@ -14,19 +14,19 @@ import Stinsen import SwiftUICollection final class MovieLibrariesViewModel: ViewModel { - + @Published var rows = [LibraryRow]() @Published var totalPages = 0 @Published var currentPage = 0 @Published var hasNextPage = false @Published var hasPreviousPage = false - + private var libraries = [BaseItemDto]() private let columns: Int - + @RouterObject var router: MovieLibrariesCoordinator.Router? - + init( columns: Int = 7 ) { @@ -35,9 +35,9 @@ final class MovieLibrariesViewModel: ViewModel { requestLibraries() } - + func requestLibraries() { - + UserViewsAPI.getUserViews( userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) @@ -60,7 +60,7 @@ final class MovieLibrariesViewModel: ViewModel { }) .store(in: &cancellables) } - + private func calculateRows() -> [LibraryRow] { guard libraries.count > 0 else { return [] } let rowCount = libraries.count / columns diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index b33992aa..c5a00a40 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -70,7 +70,7 @@ final class SeasonItemViewModel: ItemViewModel { playButtonItem = firstEpisode } } - + func routeToSeriesItem() { guard let id = item.seriesId else { return } UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/SeriesItemViewModel.swift index 5ee679b3..94f3dc6c 100644 --- a/Shared/ViewModels/SeriesItemViewModel.swift +++ b/Shared/ViewModels/SeriesItemViewModel.swift @@ -21,19 +21,19 @@ final class SeriesItemViewModel: ItemViewModel { requestSeasons() getNextUp() } - + override func playButtonText() -> String { guard let playButtonItem = playButtonItem else { return L10n.play } guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play } return episodeLocator } - + override func shouldDisplayRuntime() -> Bool { return false } - private func getNextUp() { - + private func getNextUp() { + 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) .trackActivity(loading) diff --git a/Shared/ViewModels/ServerListViewModel.swift b/Shared/ViewModels/ServerListViewModel.swift index e0f6402e..638e9555 100644 --- a/Shared/ViewModels/ServerListViewModel.swift +++ b/Shared/ViewModels/ServerListViewModel.swift @@ -11,11 +11,11 @@ import Foundation import SwiftUI class ServerListViewModel: ObservableObject { - + @Published var servers: [SwiftfinStore.State.Server] = [] - + init() { - + // Oct. 15, 2021 // 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 @@ -23,11 +23,11 @@ class ServerListViewModel: ObservableObject { let nc = SwiftfinNotificationCenter.main nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) } - + func fetchServers() { self.servers = SessionManager.main.fetchServers() } - + func userTextFor(server: SwiftfinStore.State.Server) -> String { if server.userIDs.count == 1 { return "1 user" @@ -35,12 +35,12 @@ class ServerListViewModel: ObservableObject { return "\(server.userIDs.count) users" } } - + func remove(server: SwiftfinStore.State.Server) { SessionManager.main.delete(server: server) fetchServers() } - + @objc private func didPurge() { fetchServers() } diff --git a/Shared/ViewModels/TVLibrariesViewModel.swift b/Shared/ViewModels/TVLibrariesViewModel.swift index 6d5a5ef4..1d5abe81 100644 --- a/Shared/ViewModels/TVLibrariesViewModel.swift +++ b/Shared/ViewModels/TVLibrariesViewModel.swift @@ -14,19 +14,19 @@ import Stinsen import SwiftUICollection final class TVLibrariesViewModel: ViewModel { - + @Published var rows = [LibraryRow]() @Published var totalPages = 0 @Published var currentPage = 0 @Published var hasNextPage = false @Published var hasPreviousPage = false - + private var libraries = [BaseItemDto]() private let columns: Int - + @RouterObject var router: TVLibrariesCoordinator.Router? - + init( columns: Int = 7 ) { @@ -35,9 +35,9 @@ final class TVLibrariesViewModel: ViewModel { requestLibraries() } - + func requestLibraries() { - + UserViewsAPI.getUserViews( userId: SessionManager.main.currentLogin.user.id) .trackActivity(loading) @@ -60,7 +60,7 @@ final class TVLibrariesViewModel: ViewModel { }) .store(in: &cancellables) } - + private func calculateRows() -> [LibraryRow] { guard libraries.count > 0 else { return [] } let rowCount = libraries.count / columns diff --git a/Shared/ViewModels/UserListViewModel.swift b/Shared/ViewModels/UserListViewModel.swift index 630c64a5..f93fc21f 100644 --- a/Shared/ViewModels/UserListViewModel.swift +++ b/Shared/ViewModels/UserListViewModel.swift @@ -11,24 +11,24 @@ import Foundation import SwiftUI class UserListViewModel: ViewModel { - + @Published var users: [SwiftfinStore.State.User] = [] - + let server: SwiftfinStore.State.Server - + init(server: SwiftfinStore.State.Server) { self.server = server } - + func fetchUsers() { self.users = SessionManager.main.fetchUsers(for: server) } - + func login(user: SwiftfinStore.State.User) { self.isLoading = true SessionManager.main.loginUser(server: server, user: user) } - + func remove(user: SwiftfinStore.State.User) { SessionManager.main.delete(user: user) fetchUsers() diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 14d2b115..6c0a8436 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -12,14 +12,14 @@ import Foundation import Stinsen final class UserSignInViewModel: ViewModel { - + @RouterObject var router: UserSignInCoordinator.Router? let server: SwiftfinStore.State.Server - + init(server: SwiftfinStore.State.Server) { self.server = server } - + var alertTitle: String { var message: String = "" if errorMessage?.code != ErrorMessage.noShowErrorCode { @@ -28,27 +28,27 @@ final class UserSignInViewModel: ViewModel { message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") return message } - + func login(username: String, password: String) { LogManager.shared.log.debug("Attempting to login to server at \"\(server.uri)\"", tag: "login") LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login") - + SessionManager.main.loginUser(server: server, username: username, password: password) .trackActivity(loading) .sink { completion in self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion) } receiveValue: { _ in - + } .store(in: &cancellables) } - + func cancelSignIn() { for cancellable in cancellables { cancellable.cancel() } - + self.isLoading = false } } diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index 75aefcf7..18f023cc 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -30,7 +30,7 @@ class ViewModel: ObservableObject { break case .failure(let error): let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line) - + switch error { case is ErrorResponse: let networkError: NetworkError @@ -52,7 +52,7 @@ class ViewModel: ObservableObject { self.errorMessage = networkError.errorMessage networkError.logMessage() - + case is SwiftfinStore.Errors: let swiftfinError = error as! SwiftfinStore.Errors let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, @@ -61,7 +61,7 @@ class ViewModel: ObservableObject { logConstructor: logConstructor) self.errorMessage = errorMessage LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")") - + default: let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, title: "Generic Error", diff --git a/Shared/Views/ImageView.swift b/Shared/Views/ImageView.swift index 10044b21..8176fd13 100644 --- a/Shared/Views/ImageView.swift +++ b/Shared/Views/ImageView.swift @@ -19,13 +19,13 @@ struct ImageView: View { self.blurhash = bh self.failureInitials = failureInitials } - + @ViewBuilder 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))!) .resizable() } - + @ViewBuilder private var failureImage: some View { ZStack { diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index aa6db419..80c99a13 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -365,7 +365,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -376,7 +376,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge)) @@ -391,7 +391,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -403,7 +403,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge)) @@ -416,7 +416,7 @@ struct NextUpWidget_Previews: PreviewProvider { NextUpEntryView(entry: .init(date: Date(), items: [ (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -426,7 +426,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")), + UIImage(named: "WidgetHeaderSymbol")) ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge))