migrate stinsen v1 to v2

This commit is contained in:
PangMo5 2021-09-20 20:32:04 +09:00
parent 1863d973a9
commit 16e3cd6ea5
32 changed files with 462 additions and 491 deletions

View File

@ -977,7 +977,6 @@
6267B3D92671138200A7371D /* ImageExtensions.swift */,
E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */,
621338922660107500A81A2A /* StringExtensions.swift */,
624C21742685CF60007F1390 /* SearchablePickerView.swift */,
6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */,
);
path = Extensions;
@ -1535,7 +1534,7 @@
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */,
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
@ -1551,7 +1550,7 @@
files = (
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */,
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
@ -1566,7 +1565,6 @@
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */,
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
@ -1607,7 +1605,7 @@
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
@ -1636,7 +1634,6 @@
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
@ -2246,7 +2243,7 @@
repositoryURL = "https://github.com/rundfunk47/stinsen";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
minimumVersion = 2.0.2;
};
};
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = {

View File

@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
"state": {
"branch": null,
"revision": "69261f239f0fffaf51495dadc4f8483fbfe97025",
"version": "0.6.1"
"revision": "b1a7a8a6186f2fcb28f7bda67cfc545de48b3c80",
"version": "0.6.2"
}
},
{
@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
"state": {
"branch": null,
"revision": "6dcc7c034d28fe7ac652453faeae07656f723909",
"version": "0.5.1"
"revision": "6bde3b0063ba8e7537b43744948535ca7e9e0dad",
"version": "0.5.2"
}
},
{
@ -78,8 +78,8 @@
"repositoryURL": "https://github.com/kean/Nuke.git",
"state": {
"branch": null,
"revision": "3bd3a1765bdf62d561d4c2e10e1c4fc7a010f44e",
"version": "10.3.2"
"revision": "0db18dd34998cca18e9a28bcee136f84518007a0",
"version": "10.4.1"
}
},
{
@ -105,8 +105,8 @@
"repositoryURL": "https://github.com/sushichop/Puppy",
"state": {
"branch": null,
"revision": "d670c669ce2a6ab554a903b815f461d6efc565e4",
"version": "0.3.0"
"revision": "95ce04b0e778b8d7c351876bc98bbf68328dfc9b",
"version": "0.3.1"
}
},
{
@ -114,8 +114,8 @@
"repositoryURL": "https://github.com/rundfunk47/stinsen",
"state": {
"branch": null,
"revision": "e72c20b2c4bde0d6c3a911d4eda688fee7aa3bba",
"version": "1.1.0"
"revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a",
"version": "2.0.2"
}
},
{
@ -159,8 +159,8 @@
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state": {
"branch": null,
"revision": "b9eeb1a7ea3fd6fea54ce57dee2f5794b667c8df",
"version": "0.2.0"
"revision": "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version": "0.2.1"
}
}
]

View File

@ -1,91 +1,85 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
import SwiftUI
struct PortraitItemView: View {
var item: BaseItemDto
var body: some View {
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
VStack(alignment: .leading) {
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 4, y: 2)
.shadow(radius: 4, y: 2)
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(ProgressBar())
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
.padding(0), alignment: .bottomLeading
)
.overlay(
ZStack {
if item.userData?.isFavorite ?? false {
Image(systemName: "circle.fill")
.foregroundColor(.white)
.opacity(0.6)
Image(systemName: "heart.fill")
.foregroundColor(Color(.systemRed))
.font(.system(size: 10))
}
}
.padding(.leading, 2)
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
.opacity(1), alignment: .bottomLeading)
.overlay(
ZStack {
if item.userData?.played ?? false {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
.background(Color(.white))
.clipShape(Circle().scale(0.8))
} else {
if item.userData?.unplayedItemCount != nil {
Capsule()
.fill(Color.accentColor)
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
Text(String(item.userData!.unplayedItemCount ?? 0))
.foregroundColor(.white)
.font(.caption2)
.padding(2)
}
}
}.padding(2)
.fixedSize()
.opacity(1), alignment: .topTrailing).opacity(1)
Text(item.seriesName ?? item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if item.type == "Movie" || item.type == "Series" {
Text("\(String(item.productionYear ?? 0))\(item.officialRating ?? "N/A")")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else if item.type == "Season" {
Text("\(item.name ?? "")\(String(item.productionYear ?? 0))")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else {
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
VStack(alignment: .leading) {
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100),
bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 4, y: 2)
.shadow(radius: 4, y: 2)
.overlay(Rectangle()
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
.mask(ProgressBar())
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
.padding(0), alignment: .bottomLeading)
.overlay(ZStack {
if item.userData?.isFavorite ?? false {
Image(systemName: "circle.fill")
.foregroundColor(.white)
.opacity(0.6)
Image(systemName: "heart.fill")
.foregroundColor(Color(.systemRed))
.font(.system(size: 10))
}
}
}.frame(width: 100)
}
.padding(.leading, 2)
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
.opacity(1), alignment: .bottomLeading)
.overlay(ZStack {
if item.userData?.played ?? false {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
.background(Color(.white))
.clipShape(Circle().scale(0.8))
} else {
if item.userData?.unplayedItemCount != nil {
Capsule()
.fill(Color.accentColor)
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
Text(String(item.userData!.unplayedItemCount ?? 0))
.foregroundColor(.white)
.font(.caption2)
.padding(2)
}
}
}.padding(2)
.fixedSize()
.opacity(1), alignment: .topTrailing).opacity(1)
Text(item.seriesName ?? item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if item.type == "Movie" || item.type == "Series" {
Text("\(String(item.productionYear ?? 0))\(item.officialRating ?? "N/A")")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else if item.type == "Season" {
Text("\(item.name ?? "")\(String(item.productionYear ?? 0))")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
} else {
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
}
}.frame(width: 100)
}
}

View File

@ -9,7 +9,7 @@ import SwiftUI
import Stinsen
struct ConnectToServerView: View {
@EnvironmentObject var mainRouter: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var mainRouter: MainCoordinator.Router
@StateObject var viewModel = ConnectToServerViewModel()
@State var username = ""
@State var password = ""
@ -61,7 +61,7 @@ struct ConnectToServerView: View {
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
SessionManager.current.loginWithSavedSession(user: user)
mainRouter.route(to: .mainTab)
mainRouter.root(\.mainTab)
} else {
username = publicUser.name ?? ""
viewModel.selectedPublicUser = publicUser

View File

@ -6,8 +6,8 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import JellyfinAPI
import SwiftUI
struct ProgressBar: Shape {
func path(in rect: CGRect) -> Path {
@ -31,26 +31,27 @@ struct ProgressBar: Shape {
}
struct ContinueWatchingView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
var items: [BaseItemDto]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(items, id: \.id) { item in
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
Button {
homeRouter.route(to: \.item, item)
} label: {
VStack(alignment: .leading) {
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
.frame(width: 320, height: 180)
.cornerRadius(10)
.shadow(radius: 4, y: 2)
.shadow(radius: 4, y: 2)
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(ProgressBar())
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
.padding(0), alignment: .bottomLeading
)
.overlay(Rectangle()
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
.mask(ProgressBar())
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
.padding(0), alignment: .bottomLeading)
HStack {
Text("\(item.seriesName ?? item.name ?? "")")
.font(.callout)
@ -68,11 +69,11 @@ struct ContinueWatchingView: View {
Spacer()
}.frame(width: 320, alignment: .leading)
}.padding(.top, 10)
.padding(.bottom, 5)
.padding(.bottom, 5)
}
}.padding(.trailing, 16)
}.frame(height: 215)
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
}
}
}

View File

@ -12,14 +12,11 @@ import Stinsen
import SwiftUI
final class ConnectToServerCoodinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
enum Route: NavigationRoute {}
func resolveRoute(route: Route) -> Transition {}
@ViewBuilder
func start() -> some View {
@Root var start = makeStart
@ViewBuilder func makeStart() -> some View {
ConnectToServerView()
}
}

View File

@ -11,8 +11,12 @@ import Foundation
import Stinsen
import SwiftUI
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
final class FilterCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \FilterCoordinator.start)
@Root var start = makeStart
@Binding var filters: LibraryFilters
var enabledFilterType: [FilterType]
var parentId: String = ""
@ -23,12 +27,7 @@ final class FilterCoordinator: NavigationCoordinatable {
self.parentId = parentId
}
enum Route: NavigationRoute {}
func resolveRoute(route: Route) -> Transition {}
@ViewBuilder
func start() -> some View {
@ViewBuilder func makeStart() -> some View {
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
}
}

View File

@ -8,31 +8,31 @@
*/
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class HomeCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \HomeCoordinator.start)
enum Route: NavigationRoute {
case settings
case library(viewModel: LibraryViewModel, title: String)
case item(viewModel: ItemViewModel)
@Root var start = makeStart
@Route(.modal) var settings = makeSettings
@Route(.push) var library = makeLibrary
@Route(.push) var item = makeItem
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator())
}
func resolveRoute(route: Route) -> Transition {
switch route {
case .settings:
return .modal(NavigationViewCoordinator(SettingsCoordinator()).eraseToAnyCoordinatable())
case let .library(viewModel, title):
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
case let .item(viewModel):
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
@ViewBuilder
func start() -> some View {
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
@ViewBuilder func makeStart() -> some View {
HomeView()
}
}

View File

@ -13,32 +13,32 @@ import Stinsen
import SwiftUI
final class ItemCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
var viewModel: ItemViewModel
let stack = NavigationStack(initial: \ItemCoordinator.start)
init(viewModel: ItemViewModel) {
self.viewModel = viewModel
@Root var start = makeStart
@Route(.push) var item = makeItem
@Route(.push) var library = makeLibrary
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
let itemDto: BaseItemDto
init(item: BaseItemDto) {
self.itemDto = item
}
enum Route: NavigationRoute {
case item(viewModel: ItemViewModel)
case library(viewModel: LibraryViewModel, title: String)
case videoPlayer(item: BaseItemDto)
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func resolveRoute(route: Route) -> Transition {
switch route {
case let .item(viewModel):
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
case let .library(viewModel, title):
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
case let .videoPlayer(item):
return .fullScreen(NavigationViewCoordinator(VideoPlayerCoordinator(item: item)).eraseToAnyCoordinatable())
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
@ViewBuilder
func start() -> some View {
ItemView(viewModel: self.viewModel)
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
}
@ViewBuilder func makeStart() -> some View {
ItemNavigationView(item: itemDto)
}
}

View File

@ -8,11 +8,20 @@
*/
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
final class LibraryCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \LibraryCoordinator.start)
@Root var start = makeStart
@Route(.push) var search = makeSearch
@Route(.modal) var filter = makeFilter
@Route(.push) var item = makeItem
var viewModel: LibraryViewModel
var title: String
@ -21,28 +30,21 @@ final class LibraryCoordinator: NavigationCoordinatable {
self.title = title
}
enum Route: NavigationRoute {
case search(viewModel: LibrarySearchViewModel)
case filter(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
case item(viewModel: ItemViewModel)
}
func resolveRoute(route: Route) -> Transition {
switch route {
case let .search(viewModel):
return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
case let .filter(filters, enabledFilterType, parentId):
return .modal(FilterCoordinator(filters: filters,
enabledFilterType: enabledFilterType,
parentId: parentId)
.eraseToAnyCoordinatable())
case let .item(viewModel):
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
}
}
@ViewBuilder
func start() -> some View {
@ViewBuilder func makeStart() -> some View {
LibraryView(viewModel: self.viewModel, title: title)
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
enabledFilterType: params.enabledFilterType,
parentId: params.parentId))
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
}

View File

@ -12,24 +12,22 @@ import Stinsen
import SwiftUI
final class LibraryListCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
enum Route: NavigationRoute {
case search(viewModel: LibrarySearchViewModel)
case library(viewModel: LibraryViewModel, title: String)
@Root var start = makeStart
@Route(.push) var search = makeSearch
@Route(.push) var library = makeLibrary
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func resolveRoute(route: Route) -> Transition {
switch route {
case let .search(viewModel):
return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
case let .library(viewModel, title):
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
@ViewBuilder
func start() -> some View {
func makeStart() -> some View {
LibraryListView()
}
}

View File

@ -8,55 +8,68 @@
*/
import Foundation
import Nuke
import Stinsen
import SwiftUI
#if !os(tvOS)
import WidgetKit
#endif
#if os(iOS)
final class MainCoordinator: ViewCoordinatable {
var children = ViewChild()
final class MainCoordinator: NavigationCoordinatable {
var stack: NavigationStack<MainCoordinator>
enum Route: ViewRoute {
case mainTab
case connectToServer
}
@Root var mainTab = makeMainTab
@Root var connectToServer = makeConnectToServer
func resolveRoute(route: Route) -> AnyCoordinatable {
switch route {
case .mainTab:
return MainTabCoordinator().eraseToAnyCoordinatable()
case .connectToServer:
return NavigationViewCoordinator(ConnectToServerCoodinator()).eraseToAnyCoordinatable()
init() {
if ServerEnvironment.current.server != nil, SessionManager.current.user != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.connectToServer)
}
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
#if !os(tvOS)
WidgetCenter.shared.reloadAllTimelines()
UIScrollView.appearance().keyboardDismissMode = .onDrag
#endif
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil)
}
@objc func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab)
}
@objc func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.connectToServer)
}
func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator()
}
func makeConnectToServer() -> NavigationViewCoordinator<ConnectToServerCoodinator> {
NavigationViewCoordinator(ConnectToServerCoodinator())
}
}
@ViewBuilder
func start() -> some View {
SplashView()
}
}
#elseif os(tvOS)
// temp for fixing build error
final class MainCoordinator: ViewCoordinatable {
var children = ViewChild()
// temp for fixing build error
final class MainCoordinator: NavigationCoordinatable {
var stack: NavigationStack<MainCoordinator>
enum Route: ViewRoute {
case mainTab
case connectToServer
}
@Root var mainTab = makeMainTab
@Root var connectToServer = makeMainTab
func resolveRoute(route: Route) -> AnyCoordinatable {
switch route {
case .mainTab:
return MainCoordinator().eraseToAnyCoordinatable()
case .connectToServer:
return MainCoordinator().eraseToAnyCoordinatable()
func makeMainTab() -> NavigationViewCoordinator<MainTabCoordinator> {
return NavigationViewCoordinator(MainTabCoordinator())
}
}
@ViewBuilder
func start() -> some View {
SplashView()
}
}
#endif

View File

@ -13,36 +13,29 @@ import SwiftUI
import Stinsen
final class MainTabCoordinator: TabCoordinatable {
lazy var children = TabChild(self, tabRoutes: [.home, .allMedia])
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.allMedia,
])
enum Route: TabRoute {
case home
case allMedia
@Route(tabItem: makeHomeTab) var home = makeHome
@Route(tabItem: makeTodosTab) var allMedia = makeTodos
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
return NavigationViewCoordinator(HomeCoordinator())
}
func tabItem(forTab tab: Int) -> some View {
switch tab {
case 0:
Group {
Text("Home")
Image(systemName: "house")
}
case 1:
Group {
Text("Projects")
Image(systemName: "folder")
}
default:
fatalError()
}
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
Image(systemName: "house")
Text("Home")
}
func resolveRoute(route: Route) -> AnyCoordinatable {
switch route {
case .home:
return NavigationViewCoordinator(HomeCoordinator()).eraseToAnyCoordinatable()
case .allMedia:
return NavigationViewCoordinator(LibraryListCoordinator()).eraseToAnyCoordinatable()
}
func makeTodos() -> NavigationViewCoordinator<LibraryListCoordinator> {
return NavigationViewCoordinator(LibraryListCoordinator())
}
@ViewBuilder func makeTodosTab(isActive: Bool) -> some View {
Image(systemName: "folder")
Text("All Media")
}
}

View File

@ -10,28 +10,25 @@
import Foundation
import Stinsen
import SwiftUI
import JellyfinAPI
final class SearchCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \SearchCoordinator.start)
@Root var start = makeStart
@Route(.push) var item = makeItem
var viewModel: LibrarySearchViewModel
init(viewModel: LibrarySearchViewModel) {
self.viewModel = viewModel
}
enum Route: NavigationRoute {
case item(viewModel: ItemViewModel)
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func resolveRoute(route: Route) -> Transition {
switch route {
case let .item(viewModel):
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
}
}
@ViewBuilder
func start() -> some View {
@ViewBuilder func makeStart() -> some View {
LibrarySearchView(viewModel: self.viewModel)
}
}

View File

@ -12,21 +12,16 @@ import Stinsen
import SwiftUI
final class SettingsCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \SettingsCoordinator.start)
enum Route: NavigationRoute {
case serverDetail
@Root var start = makeStart
@Route(.push) var serverDetail = makeServerDetail
@ViewBuilder func makeServerDetail() -> some View {
ServerDetailView()
}
func resolveRoute(route: Route) -> Transition {
switch route {
case .serverDetail:
return .push(ServerDetailView().eraseToAnyView())
}
}
@ViewBuilder
func start() -> some View {
@ViewBuilder func makeStart() -> some View {
SettingsView(viewModel: .init())
}
}

View File

@ -13,19 +13,16 @@ import Stinsen
import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root var start = makeStart
var item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
enum Route: NavigationRoute {}
func resolveRoute(route: Route) -> Transition {}
@ViewBuilder
func start() -> some View {
@ViewBuilder func makeStart() -> some View {
VideoPlayerView(item: item)
}
}

View File

@ -11,9 +11,9 @@ import Foundation
import SwiftUI
struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel = HomeViewModel()
@State var showingSettings = false
init() {
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
let barAppearance = UINavigationBar.appearance()
@ -43,16 +43,19 @@ struct HomeView: View {
.font(.title2)
.fontWeight(.bold)
Spacer()
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
}) {
Button {
homeRouter
.route(to: \.library, (viewModel: .init(parentID: libraryID,
filters: viewModel.recentFilterSet),
title: library?.name ?? ""))
} label: {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
}
}.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.trailing, 16)
LatestMediaView(viewModel: .init(libraryID: libraryID))
}
}
@ -68,14 +71,11 @@ struct HomeView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
showingSettings = true
homeRouter.route(to: \.settings)
} label: {
Image(systemName: "gear")
}
}
}
.fullScreenCover(isPresented: $showingSettings) {
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
}
}
}

View File

@ -5,41 +5,41 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import Introspect
import JellyfinAPI
import SwiftUI
class VideoPlayerItem: ObservableObject {
@Published var shouldShowPlayer: Bool = false
@Published var itemToPlay: BaseItemDto = BaseItemDto()
@Published var itemToPlay = BaseItemDto()
}
// Intermediary view for ItemView to set navigation bar settings
struct ItemNavigationView: View {
private let item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
var body: some View {
ItemView(item: item)
.navigationBarTitle("", displayMode: .inline)
}
}
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.
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
@State private var viewDidLoad: Bool = false
@State private var orientation: UIDeviceOrientation = .unknown
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
@StateObject private var videoPlayerItem = VideoPlayerItem()
@Environment(\.horizontalSizeClass) private var hSizeClass
@Environment(\.verticalSizeClass) private var vSizeClass
private let viewModel: ItemViewModel
init(item: BaseItemDto) {
switch item.itemType {
case .movie:
@ -56,14 +56,20 @@ fileprivate struct ItemView: View {
}
var body: some View {
if hSizeClass == .compact && vSizeClass == .regular {
ItemPortraitMainView(videoIsLoading: $videoIsLoading)
.environmentObject(videoPlayerItem)
.environmentObject(viewModel)
} else {
ItemLandscapeMainView(videoIsLoading: $videoIsLoading)
.environmentObject(videoPlayerItem)
.environmentObject(viewModel)
Group {
if hSizeClass == .compact && vSizeClass == .regular {
ItemPortraitMainView(videoIsLoading: $videoIsLoading)
.environmentObject(videoPlayerItem)
.environmentObject(viewModel)
} else {
ItemLandscapeMainView(videoIsLoading: $videoIsLoading)
.environmentObject(videoPlayerItem)
.environmentObject(viewModel)
}
}
.onReceive(videoPlayerItem.$shouldShowPlayer) { flag in
guard flag else { return }
self.itemRouter.route(to: \.videoPlayer, viewModel.item)
}
}
}

View File

@ -1,45 +1,45 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
struct ItemLandscapeMainView: View {
@Binding private var videoIsLoading: Bool
@EnvironmentObject private var viewModel: ItemViewModel
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
init(videoIsLoading: Binding<Bool>) {
self._videoIsLoading = videoIsLoading
}
// MARK: innerBody
private var innerBody: some View {
HStack {
// MARK: Sidebar Image
VStack {
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130),
bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 130, height: 195)
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
if let playButtonItem = viewModel.playButtonItem {
self.videoPlayerItem.itemToPlay = playButtonItem
self.videoPlayerItem.shouldShowPlayer = true
}
} label: {
// MARK: Play
HStack {
Image(systemName: "play.fill")
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white)
@ -53,18 +53,19 @@ struct ItemLandscapeMainView: View {
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
.cornerRadius(10)
}.disabled(viewModel.playButtonItem == nil)
Spacer()
}
ScrollView {
VStack(alignment: .leading) {
// MARK: ItemLandscapeTopBarView
ItemLandscapeTopBarView()
.environmentObject(viewModel)
// MARK: ItemViewBody
if let episodeViewModel = viewModel as? SeasonItemViewModel {
CardVStackView(items: episodeViewModel.episodes)
} else {
@ -75,32 +76,20 @@ struct ItemLandscapeMainView: View {
}
}
}
// MARK: body
var body: some View {
VStack {
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
loadBinding: $videoIsLoading,
pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: true)
.edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true)
}, isActive: $videoPlayerItem.shouldShowPlayer) {
EmptyView()
}
ZStack {
// MARK: Backdrop
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.blur(radius: 4)
// iPadOS is making the view go all the way to the edge.
// We have to accomodate this here
if UIDevice.current.userInterfaceIdiom == .pad {

View File

@ -37,19 +37,6 @@ struct ItemPortraitMainView: View {
// MARK: body
var body: some View {
VStack(alignment: .leading) {
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
loadBinding: $videoIsLoading,
pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: true)
.edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true)
}, isActive: $videoPlayerItem.shouldShowPlayer) {
EmptyView()
}
// MARK: ParallaxScrollView
ParallaxHeaderScrollView(header: portraitHeaderView,
staticOverlayView: portraitStaticOverlayView,

View File

@ -28,7 +28,7 @@ extension UIWindow {
struct DeviceShakeViewModifier: ViewModifier {
let action: () -> Void
func body(content: Content) -> some View {
func body(content: Self.Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
@ -228,7 +228,7 @@ struct JellyfinPlayerApp: App {
})
.withHostingWindow { window in
window?
.rootViewController = PreferenceUIHostingController(wrappedView: CoordinatorView(MainCoordinator())
.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view()
.environment(\.managedObjectContext, persistenceController.container.viewContext))
}
.onShake {

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct LatestMediaView: View {
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel: LatestMediaViewModel
var body: some View {
@ -18,7 +18,7 @@ struct LatestMediaView: View {
ForEach(viewModel.items, id: \.id) { item in
if item.type == "Series" || item.type == "Movie" {
Button {
homeRouter.route(to: .item(viewModel: .init(id: item.id!)))
homeRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item)
}

View File

@ -10,7 +10,7 @@ import SwiftUI
import Stinsen
struct LibraryFilterView: View {
@EnvironmentObject var filterRouter: NavigationRouter<FilterCoordinator.Route>
@EnvironmentObject var filterRouter: FilterCoordinator.Router
@Environment(\.presentationMode) var presentationMode
@Binding var filters: LibraryFilters
var parentId: String = ""
@ -66,7 +66,7 @@ struct LibraryFilterView: View {
Button {
viewModel.resetFilters()
self.filters = viewModel.modifiedFilters
filterRouter.dismiss()
filterRouter.dismissCoordinator()
} label: {
Text("Reset")
}
@ -76,7 +76,7 @@ struct LibraryFilterView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
filterRouter.dismiss()
filterRouter.dismissCoordinator()
} label: {
Image(systemName: "xmark")
}
@ -85,7 +85,7 @@ struct LibraryFilterView: View {
Button {
viewModel.updateModifiedFilter()
self.filters = viewModel.modifiedFilters
filterRouter.dismiss()
filterRouter.dismissCoordinator()
} label: {
Text("Apply")
}

View File

@ -10,14 +10,15 @@ import Stinsen
import SwiftUI
struct LibraryListView: View {
@EnvironmentObject var libraryListRouter: NavigationRouter<LibraryListCoordinator.Route>
@EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router
@StateObject var viewModel = LibraryListViewModel()
var body: some View {
ScrollView {
LazyVStack {
Button {
libraryListRouter.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites"))
libraryListRouter.route(to: \.library,
(viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: "Favorites"))
} label: {
ZStack {
HStack {
@ -62,7 +63,9 @@ struct LibraryListView: View {
ForEach(viewModel.libraries, id: \.id) { library in
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
Button {
libraryListRouter.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? ""))
libraryListRouter.route(to: \.library,
(viewModel: LibraryViewModel(parentID: library.id),
title: library.name ?? ""))
} label: {
ZStack {
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
@ -99,7 +102,7 @@ struct LibraryListView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
libraryListRouter.route(to: .search(viewModel: .init(parentID: nil)))
libraryListRouter.route(to: \.search, LibrarySearchViewModel(parentID: nil))
} label: {
Image(systemName: "magnifyingglass")
}

View File

@ -11,7 +11,7 @@ import Stinsen
import SwiftUI
struct LibrarySearchView: View {
@EnvironmentObject var searchRouter: NavigationRouter<SearchCoordinator.Route>
@EnvironmentObject var searchRouter: SearchCoordinator.Router
@StateObject var viewModel: LibrarySearchViewModel
@State private var searchQuery = ""
@ -81,7 +81,7 @@ struct LibrarySearchView: View {
LazyVGrid(columns: tracks) {
ForEach(items, id: \.id) { item in
Button {
searchRouter.route(to: .item(viewModel: .init(id: item.id!)))
searchRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item)
}

View File

@ -10,7 +10,7 @@ import Stinsen
import SwiftUI
struct LibraryView: View {
@EnvironmentObject var libraryRouter: NavigationRouter<LibraryCoordinator.Route>
@EnvironmentObject var libraryRouter: LibraryCoordinator.Router
@StateObject var viewModel: LibraryViewModel
var title: String
@ -36,7 +36,7 @@ struct LibraryView: View {
ForEach(viewModel.items, id: \.id) { item in
if item.type != "Folder" {
Button {
libraryRouter.route(to: .item(viewModel: .init(id: item.id!)))
libraryRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item)
}
@ -96,11 +96,11 @@ struct LibraryView: View {
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
.onTapGesture {
libraryRouter
.route(to: .filter(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
parentId: viewModel.parentID ?? ""))
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
parentId: viewModel.parentID ?? ""))
}
Button {
libraryRouter.route(to: .search(viewModel: .init(parentID: viewModel.parentID)))
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
} label: {
Image(systemName: "magnifyingglass")
}

View File

@ -11,7 +11,7 @@ import Stinsen
import SwiftUI
struct NextUpView: View {
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: HomeCoordinator.Router
var items: [BaseItemDto]
@ -25,7 +25,7 @@ struct NextUpView: View {
LazyHStack {
ForEach(items, id: \.id) { item in
Button {
homeRouter.route(to: .item(viewModel: .init(id: item.id!)))
homeRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item)
}

View File

@ -6,15 +6,16 @@
*/
import CoreData
import SwiftUI
import Defaults
import Stinsen
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var viewModel: SettingsViewModel
@Binding var close: Bool
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
@ -25,101 +26,104 @@ struct SettingsView: View {
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
var body: some View {
NavigationView {
Form {
Section(header: EmptyView()) {
Form {
Section(header: EmptyView()) {
HStack {
Text("User")
Spacer()
Text(SessionManager.current.user.username ?? "")
.foregroundColor(.jellyfinPurple)
}
Button {
settingsRouter.route(to: \.serverDetail)
} label: {
HStack {
Text("User")
Text("Server")
Spacer()
Text(SessionManager.current.user.username ?? "")
Text(ServerEnvironment.current.server.name ?? "")
.foregroundColor(.jellyfinPurple)
}
NavigationLink(
destination: ServerDetailView(),
label: {
HStack {
Text("Server")
Spacer()
Text(ServerEnvironment.current.server.name ?? "")
.foregroundColor(.jellyfinPurple)
}
})
Button {
close = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
SessionManager.current.logout()
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
}
} label: {
Text("Sign out")
.font(.callout)
}
}
Section(header: Text("Playback")) {
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
Picker("Jump Forward Length", selection: $jumpForwardLength) {
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
Text(length.label).tag(length.rawValue)
}
}
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
Text(length.label).tag(length.rawValue)
}
Image(systemName: "chevron.right")
}
}
Section(header: Text("Accessibility")) {
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
SearchablePicker(label: "Preferred subtitle language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto },
set: {autoSelectSubtitlesLangcode = $0.isoCode}
)
)
SearchablePicker(label: "Preferred audio language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto },
set: { autoSelectAudioLangcode = $0.isoCode}
)
)
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
ForEach(self.viewModel.appearances, id: \.self) { appearance in
Text(appearance.localizedName).tag(appearance.rawValue)
}
}.onChange(of: appAppearance, perform: { value in
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
})
Button {
settingsRouter.dismissCoordinator()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
SessionManager.current.logout()
let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil)
}
} label: {
Text("Sign out")
.font(.callout)
}
}
.navigationBarTitle("Settings", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
close = false
} label: {
Image(systemName: "xmark")
Section(header: Text("Playback")) {
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
Picker("Jump Forward Length", selection: $jumpForwardLength) {
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
Text(length.label).tag(length.rawValue)
}
}
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
Text(length.label).tag(length.rawValue)
}
}
}
Section(header: Text("Accessibility")) {
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
SearchablePicker(label: "Preferred subtitle language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(get: {
viewModel.langs
.first(where: { $0.isoCode == autoSelectSubtitlesLangcode
}) ??
.auto
},
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
SearchablePicker(label: "Preferred audio language",
options: viewModel.langs,
optionToString: { $0.name },
selected: Binding<TrackLanguage>(get: {
viewModel.langs
.first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
.auto
},
set: { autoSelectAudioLangcode = $0.isoCode }))
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
ForEach(self.viewModel.appearances, id: \.self) { appearance in
Text(appearance.localizedName).tag(appearance.rawValue)
}
}.onChange(of: appAppearance, perform: { _ in
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
})
}
}
.navigationBarTitle("Settings", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
settingsRouter.dismissCoordinator()
} label: {
Image(systemName: "xmark")
}
}
}
}

View File

@ -11,16 +11,16 @@ import Stinsen
import SwiftUI
struct SplashView: View {
@EnvironmentObject var mainRouter: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var mainRouter: MainCoordinator.Router
@StateObject var viewModel = SplashViewModel()
var body: some View {
ProgressView()
.onReceive(viewModel.$isLoggedIn) { flag in
if flag {
mainRouter.route(to: .mainTab)
mainRouter.root(\.mainTab)
} else {
mainRouter.route(to: .connectToServer)
mainRouter.root(\.connectToServer)
}
}
}

View File

@ -28,7 +28,7 @@ protocol PlayerViewControllerDelegate: AnyObject {
class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener {
@RouterObject
var main: ViewRouter<MainCoordinator.Route>?
var main: MainCoordinator.Router?
weak var delegate: PlayerViewControllerDelegate?
@ -538,7 +538,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
case .error(401, _, _, _):
self.delegate?.exitPlayer(self)
SessionManager.current.logout()
main?.route(to: .connectToServer)
main?.root(\.connectToServer)
case .error:
self.delegate?.exitPlayer(self)
}
@ -1072,12 +1072,12 @@ struct VideoPlayerView: View {
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto
@RouterObject var playerRouter: NavigationRouter<VideoPlayerCoordinator.Route>?
@RouterObject var playerRouter: VideoPlayerCoordinator.Router?
let loadBinding: Binding<Bool>
class Coordinator: NSObject, PlayerViewControllerDelegate {
let parent: VLCPlayerWithControls
var parent: VLCPlayerWithControls
let loadBinding: Binding<Bool>
init(parent: VLCPlayerWithControls, loadBinding: Binding<Bool>) {
@ -1094,7 +1094,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
}
func exitPlayer(_ viewController: PlayerViewController) {
parent.playerRouter?.dismiss()
parent.playerRouter?.dismissCoordinator()
}
}

View File

@ -14,7 +14,7 @@ final class AppURLHandler {
static let deepLinkScheme = "jellyfin"
@RouterObject
var router: NavigationRouter<HomeCoordinator.Route>?
var router: HomeCoordinator.Router?
enum AppURLState {
case launched
@ -54,7 +54,7 @@ extension AppURLHandler {
}
return true
}
func processLaunchedURLIfNeeded() {
guard let launchURL = launchURL else { return }
if processDeepLink(url: launchURL) {
@ -78,7 +78,7 @@ extension AppURLHandler {
if url.pathComponents[safe: 2]?.lowercased() == "items",
let itemID = url.pathComponents[safe: 3]
{
router?.route(to: .item(viewModel: .init(id: itemID)))
// router?.route(to: \.item(item: item))
return true
}

View File

@ -14,7 +14,7 @@ import Stinsen
final class ConnectToServerViewModel: ViewModel {
@RouterObject
var main: ViewRouter<MainCoordinator.Route>?
var main: MainCoordinator.Router?
@Published var isConnectedServer = false
@ -60,13 +60,12 @@ final class ConnectToServerViewModel: ViewModel {
}
func connectToServer() {
#if targetEnvironment(simulator)
if uriSubject.value == "localhost" {
uriSubject.value = "http://localhost:8096"
}
if uriSubject.value == "localhost" {
uriSubject.value = "http://localhost:8096"
}
#endif
LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer")
ServerEnvironment.current.create(with: uriSubject.value)
.trackActivity(loading)
@ -112,7 +111,7 @@ final class ConnectToServerViewModel: ViewModel {
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
completion: completion)
}, receiveValue: { [weak self] _ in
self?.main?.route(to: .mainTab)
self?.main?.root(\.mainTab)
})
.store(in: &cancellables)
}