tvOS - Revamp Connect Flow (#563)

This commit is contained in:
Ethan Pippin 2022-09-07 23:52:19 -06:00 committed by GitHub
parent d1414f2855
commit 859a47803f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 524 additions and 302 deletions

View File

@ -21,14 +21,6 @@ final class MainCoordinator: NavigationCoordinatable {
@Root
var liveTV = makeLiveTV
@ViewBuilder
func customize(_ view: AnyView) -> some View {
view.background {
Color.black
.ignoresSafeArea()
}
}
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
@ -39,6 +31,8 @@ final class MainCoordinator: NavigationCoordinatable {
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.label]
// Notification setup for state
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))

View File

@ -18,7 +18,7 @@ enum SwiftfinStore {
// Relationships are represented by the related object's IDs or value
enum State {
struct Server {
struct Server: Hashable, Identifiable {
let uris: Set<String>
let currentURI: String
let name: String
@ -27,7 +27,7 @@ enum SwiftfinStore {
let version: String
let userIDs: [String]
fileprivate init(
init(
uris: Set<String>,
currentURI: String,
name: String,
@ -58,7 +58,7 @@ enum SwiftfinStore {
}
}
struct User {
struct User: Hashable, Identifiable {
let username: String
let id: String
let serverID: String

View File

@ -26,7 +26,7 @@ final class ConnectToServerViewModel: ViewModel {
@RouterObject
var router: ConnectToServerCoodinator.Router?
@Published
var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
var discoveredServers: [SwiftfinStore.State.Server] = []
@Published
var searching = false
@Published
@ -102,15 +102,26 @@ final class ConnectToServerViewModel: ViewModel {
discoveredServers.removeAll()
searching = true
var _discoveredServers: Set<SwiftfinStore.State.Server> = []
discovery.locateServer { server in
if let server = server {
_discoveredServers.insert(.init(
uris: [],
currentURI: server.url.absoluteString,
name: server.name,
id: server.id,
os: "",
version: "",
usersIDs: []
))
}
}
// Timeout after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.searching = false
}
discovery.locateServer { [self] server in
if let server = server {
discoveredServers.insert(server)
}
self.discoveredServers = _discoveredServers.sorted(by: { $0.name < $1.name })
}
}

View File

@ -7,6 +7,7 @@
//
import Foundation
import JellyfinAPI
import SwiftUI
class UserListViewModel: ViewModel {
@ -21,6 +22,7 @@ class UserListViewModel: ViewModel {
super.init()
JellyfinAPIAPI.basePath = server.currentURI
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeCurrentLoginURI(_:)))
}

View File

@ -15,15 +15,6 @@ struct JellyfinPlayer_tvOSApp: App {
var body: some Scene {
WindowGroup {
MainCoordinator().view()
.onAppear {
JellyfinPlayer_tvOSApp.setupAppearance()
}
}
}
static func setupAppearance() {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
windowScene?.windows.first?.overrideUserInterfaceStyle = .dark
}
}

View File

@ -62,7 +62,7 @@ struct CinematicNextUpCardView: View {
}
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
.padding(.top)
}
.padding(.vertical)

View File

@ -63,7 +63,7 @@ struct CinematicResumeCardView: View {
}
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
.padding(.top)
.contextMenu {
Button(role: .destructive) {

View File

@ -8,11 +8,6 @@
import SwiftUI
enum PosterButtonWidth {
static let landscape = 490.0
static let portrait = 250.0
}
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: View {
private let item: Item
@ -26,12 +21,7 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
private let singleImage: Bool
private var itemWidth: CGFloat {
switch type {
case .portrait:
return PosterButtonWidth.portrait * itemScale
case .landscape:
return PosterButtonWidth.landscape * itemScale
}
type.width * itemScale
}
private init(
@ -76,7 +66,7 @@ struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
.posterStyle(type: type, width: itemWidth)
}
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
.contextMenu(menuItems: {
contextMenu(item)
})

View File

@ -0,0 +1,53 @@
//
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct ServerButton: View {
let server: SwiftfinStore.State.Server
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
HStack {
Image(systemName: "server.rack")
.font(.system(size: 72))
.foregroundColor(.primary)
VStack(alignment: .leading, spacing: 5) {
Text(server.name)
.font(.title2)
.foregroundColor(.primary)
Text(server.currentURI)
.font(.footnote)
.disabled(true)
.foregroundColor(.secondary)
}
Spacer()
}
}
}
}
extension ServerButton {
init(server: SwiftfinStore.State.Server) {
self.server = server
self.onSelect = {}
}
func onSelect(_ action: @escaping () -> Void) -> Self {
var copy = self
copy.onSelect = action
return copy
}
}

View File

@ -0,0 +1,57 @@
//
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct UserProfileButton: View {
@FocusState
private var isFocused: Bool
let user: UserDto
private var action: () -> Void
init(user: UserDto) {
self.user = user
self.action = {}
}
init(user: SwiftfinStore.State.User) {
self.init(user: .init(name: user.username, id: user.id))
}
var body: some View {
VStack(alignment: .center) {
Button {
action()
} label: {
ImageView(user.profileImageSource(maxWidth: 250, maxHeight: 250))
.failure {
Image(systemName: "person.fill")
.resizable()
.padding2()
}
.frame(width: 200, height: 200)
}
.buttonStyle(.card)
.focused($isFocused)
Text(user.name ?? .emptyDash)
.foregroundColor(isFocused ? .primary : .secondary)
}
}
}
extension UserProfileButton {
func onSelect(_ action: @escaping () -> Void) -> Self {
var copy = self
copy.action = action
return copy
}
}

View File

@ -12,16 +12,17 @@ import SwiftUI
struct ConnectToServerView: View {
@StateObject
@ObservedObject
var viewModel: ConnectToServerViewModel
@State
var uri = ""
private var uri = ""
@Default(.defaultHTTPScheme)
var defaultHTTPScheme
private var defaultHTTPScheme
var body: some View {
List {
@ViewBuilder
private var connectForm: some View {
VStack(alignment: .leading) {
Section {
TextField(L10n.serverURL, text: $uri)
.disableAutocorrection(true)
@ -37,43 +38,94 @@ struct ConnectToServerView: View {
viewModel.connectToServer(uri: uri)
} label: {
HStack {
L10n.connect.text
Spacer()
if viewModel.isLoading {
ProgressView()
}
L10n.connect.text
.bold()
.font(.callout)
}
.frame(height: 75)
.frame(maxWidth: .infinity)
.background(viewModel.isLoading || uri.isEmpty ? .secondary : Color.jellyfinPurple)
}
.disabled(viewModel.isLoading || uri.isEmpty)
.buttonStyle(.plain)
} header: {
L10n.connectToJellyfinServer.text
}
}
}
Section(header: L10n.localServers.text) {
if viewModel.searching {
ProgressView()
@ViewBuilder
private var searchingDiscoverServers: some View {
HStack(spacing: 5) {
ProgressView()
L10n.searchingDots.text
.foregroundColor(.secondary)
}
}
@ViewBuilder
private var noLocalServersFound: some View {
L10n.noLocalServersFound.text
.font(.callout)
.foregroundColor(.secondary)
}
@ViewBuilder
private var localServers: some View {
VStack(alignment: .center) {
HStack {
L10n.localServers.text
.font(.title3)
.fontWeight(.semibold)
SFSymbolButton(systemName: "arrow.clockwise") {
viewModel.discoverServers()
}
ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in
Button(action: {
viewModel.connectToServer(uri: discoveredServer.url.absoluteString)
}, label: {
HStack {
Text(discoveredServer.name)
.font(.headline)
Text("\(discoveredServer.host)")
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
if viewModel.isLoading {
ProgressView()
.frame(width: 30, height: 30)
.disabled(viewModel.searching || viewModel.isLoading)
}
if viewModel.searching {
searchingDiscoverServers
.frame(maxHeight: .infinity)
} else {
if viewModel.discoveredServers.isEmpty {
noLocalServersFound
.frame(maxHeight: .infinity)
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.discoveredServers, id: \.self) { server in
ServerButton(server: server)
.onSelect {
viewModel.connectToServer(uri: server.currentURI)
}
}
}
})
.padding()
}
}
}
.onAppear(perform: self.viewModel.discoverServers)
.headerProminence(.increased)
}
}
var body: some View {
HStack(alignment: .top) {
connectForm
.frame(maxWidth: .infinity)
localServers
.frame(maxWidth: .infinity)
}
.navigationTitle(L10n.connect.text)
.onAppear {
viewModel.discoverServers()
}
.alert(item: $viewModel.errorMessage) { _ in
Alert(
@ -82,6 +134,12 @@ struct ConnectToServerView: View {
dismissButton: .cancel()
)
}
.navigationTitle(L10n.connect)
.alert(item: $viewModel.addServerURIPayload) { _ in
Alert(
title: L10n.existingServer.text,
message: L10n.serverAlreadyExistsPrompt(viewModel.addServerURIPayload?.server.name ?? .emptyDash).text,
dismissButton: .cancel()
)
}
}
}

View File

@ -58,7 +58,7 @@ struct ContinueWatchingCard: View {
}
.frame(width: 500, height: 281.25)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
.padding(.top)
VStack(alignment: .leading) {

View File

@ -37,7 +37,7 @@ extension ItemView.AboutView {
.padding2()
.frame(width: 700, height: 405)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
}
}
}

View File

@ -32,7 +32,7 @@ extension ItemView {
.frame(height: 100)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
Button {
viewModel.toggleFavoriteState()
@ -49,7 +49,7 @@ extension ItemView {
.frame(height: 100)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
}
}
}

View File

@ -47,7 +47,7 @@ extension ItemView {
.cornerRadius(10)
}
.focused($isFocused)
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
.contextMenu {
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
Button {

View File

@ -38,7 +38,7 @@ struct EpisodeCard: View {
}
.frame(width: 550, height: 308)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
Button {
router.route(to: \.item, episode)
@ -79,7 +79,7 @@ struct EpisodeCard: View {
.frame(width: 510, height: 220)
.padding()
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
}
}
}

View File

@ -59,7 +59,7 @@ extension SeriesEpisodesView {
.foregroundColor(.black)
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.id(season)
.focused($focusedSeason, equals: season)
}

View File

@ -36,7 +36,7 @@ struct LatestInLibraryView: View {
}
.posterStyle(type: .portrait, width: 250)
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
}
.onSelect { item in
router.route(to: \.item, item)

View File

@ -6,55 +6,32 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import CoreStore
import CollectionView
import SwiftUI
struct ServerListView: View {
@EnvironmentObject
private var serverListRouter: ServerListCoordinator.Router
private var router: ServerListCoordinator.Router
@ObservedObject
var viewModel: ServerListViewModel
@State
private var longPressedServer: SwiftfinStore.State.Server?
@ViewBuilder
private var listView: some View {
ScrollView {
LazyVStack {
ForEach(viewModel.servers, id: \.id) { server in
Button {
serverListRouter.route(to: \.userList, server)
} label: {
HStack {
Image(systemName: "server.rack")
.font(.system(size: 72))
.foregroundColor(.primary)
VStack(alignment: .leading, spacing: 5) {
Text(server.name)
.font(.title2)
.foregroundColor(.primary)
Text(server.currentURI)
.font(.footnote)
.disabled(true)
.foregroundColor(.secondary)
Text(viewModel.userTextFor(server: server))
.font(.footnote)
.foregroundColor(.primary)
}
Spacer()
ServerButton(server: server)
.onSelect {
router.route(to: \.userList, server)
}
}
.padding(.horizontal, 100)
.contextMenu {
Button(role: .destructive) {
viewModel.remove(server: server)
} label: {
Label(L10n.remove, systemImage: "trash")
.onLongPressGesture {
longPressedServer = server
}
}
.padding(.horizontal, 100)
}
}
.padding(.top, 50)
@ -64,24 +41,22 @@ struct ServerListView: View {
@ViewBuilder
private var noServerView: some View {
VStack {
VStack(spacing: 50) {
L10n.connectToJellyfinServerStart.text
.frame(minWidth: 50, maxWidth: 500)
.frame(maxWidth: 500)
.multilineTextAlignment(.center)
.font(.body)
Button {
serverListRouter.route(to: \.connectToServer)
router.route(to: \.connectToServer)
} label: {
L10n.connect.text
.bold()
.font(.callout)
.padding(.vertical)
.padding(.horizontal, 30)
.frame(width: 400, height: 75)
.background(Color.jellyfinPurple)
}
.padding(.top, 40)
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
}
}
@ -95,34 +70,34 @@ struct ServerListView: View {
}
}
@ViewBuilder
private var trailingToolbarContent: some View {
if viewModel.servers.isEmpty {
EmptyView()
} else {
Button {
serverListRouter.route(to: \.connectToServer)
} label: {
Image(systemName: "plus.circle.fill")
}
.contextMenu {
Button {
serverListRouter.route(to: \.basicAppSettings)
} label: {
L10n.settings.text
}
}
}
}
var body: some View {
innerBody
.navigationTitle(L10n.servers)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
trailingToolbarContent
.if(!viewModel.servers.isEmpty) { view in
view.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
router.route(to: \.connectToServer)
} label: {
Image(systemName: "plus.circle.fill")
}
.contextMenu {
Button {
router.route(to: \.basicAppSettings)
} label: {
L10n.settings.text
}
}
}
}
}
.alert(item: $longPressedServer) { server in
Alert(
title: Text(server.name),
primaryButton: .destructive(L10n.remove.text, action: { viewModel.remove(server: server) }),
secondaryButton: .cancel()
)
}
.onAppear {
viewModel.fetchServers()
}

View File

@ -6,6 +6,8 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import CollectionView
import JellyfinAPI
import SwiftUI
struct UserListView: View {
@ -15,47 +17,39 @@ struct UserListView: View {
@ObservedObject
var viewModel: UserListViewModel
@State
private var longPressedUser: SwiftfinStore.State.User?
@ViewBuilder
private var listView: some View {
ScrollView {
LazyVStack {
ForEach(viewModel.users, id: \.id) { user in
Button {
viewModel.signIn(user: user)
} label: {
HStack {
Text(user.username)
.font(.title2)
Spacer()
if viewModel.isLoading {
ProgressView()
}
}
}
.padding(.horizontal, 100)
.contextMenu {
Button(role: .destructive) {
viewModel.remove(user: user)
} label: {
Label(L10n.remove, systemImage: "trash")
}
}
CollectionView(items: viewModel.users) { _, user, _ in
UserProfileButton(user: user)
.onSelect {
viewModel.signIn(user: user)
}
.onLongPressGesture {
longPressedUser = user
}
}
.padding(.top, 50)
}
.padding(.top, 50)
.layout { _, layoutEnvironment in
.grid(
layoutEnvironment: layoutEnvironment,
layoutMode: .adaptive(withMinItemSize: 250),
itemSpacing: 20,
lineSpacing: 20,
sectionInsets: .init(top: 20, leading: 20, bottom: 20, trailing: 20)
)
}
.padding(50)
}
@ViewBuilder
private var noUserView: some View {
VStack {
VStack(spacing: 50) {
L10n.signInGetStarted.text
.frame(minWidth: 50, maxWidth: 500)
.frame(maxWidth: 500)
.multilineTextAlignment(.center)
.font(.callout)
.font(.body)
Button {
userListRouter.route(to: \.userSignIn, viewModel.server)
@ -63,46 +57,51 @@ struct UserListView: View {
L10n.signIn.text
.bold()
.font(.callout)
.frame(width: 400, height: 75)
.background(Color.jellyfinPurple)
}
.padding(.top, 40)
}
}
@ViewBuilder
private var innerBody: some View {
if viewModel.users.isEmpty {
noUserView
.offset(y: -50)
} else {
listView
}
}
@ViewBuilder
private var toolbarContent: some View {
if viewModel.users.isEmpty {
EmptyView()
} else {
HStack {
Button {
userListRouter.route(to: \.userSignIn, viewModel.server)
} label: {
Image(systemName: "person.crop.circle.fill.badge.plus")
}
}
.buttonStyle(.card)
}
}
var body: some View {
innerBody
.navigationTitle(viewModel.server.name)
.toolbar {
ZStack {
ImageView(ImageAPI.getSplashscreenWithRequestBuilder().url)
.ignoresSafeArea()
Color.black
.opacity(0.9)
.ignoresSafeArea()
if viewModel.users.isEmpty {
noUserView
.offset(y: -50)
} else {
listView
}
}
.navigationTitle(viewModel.server.name)
.if(!viewModel.users.isEmpty) { view in
view.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
toolbarContent
Button {
userListRouter.route(to: \.userSignIn, viewModel.server)
} label: {
Image(systemName: "person.crop.circle.fill.badge.plus")
}
}
}
.onAppear {
viewModel.fetchUsers()
}
}
.alert(item: $longPressedUser) { user in
Alert(
title: Text(user.username),
primaryButton: .destructive(L10n.remove.text, action: { viewModel.remove(user: user) }),
secondaryButton: .cancel()
)
}
.onAppear {
viewModel.fetchUsers()
}
}
}

View File

@ -6,18 +6,156 @@
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import CollectionView
import JellyfinAPI
import Stinsen
import SwiftUI
struct UserSignInView: View {
enum FocusedField {
case username
case password
}
@ObservedObject
var viewModel: UserSignInViewModel
@State
private var username: String = ""
@State
private var password: String = ""
@State
private var presentQuickConnect: Bool = false
@FocusState
private var focusedField: FocusedField?
@ViewBuilder
private var signInForm: some View {
VStack(alignment: .leading) {
Section {
TextField(L10n.username, text: $username)
.disableAutocorrection(true)
.autocapitalization(.none)
.focused($focusedField, equals: .username)
SecureField(L10n.password, text: $password)
.disableAutocorrection(true)
.autocapitalization(.none)
.focused($focusedField, equals: .password)
Button {
viewModel.signIn(username: username, password: password)
} label: {
HStack {
if viewModel.isLoading {
ProgressView()
}
L10n.connect.text
.bold()
.font(.callout)
}
.frame(height: 75)
.frame(maxWidth: .infinity)
.background(viewModel.isLoading || username.isEmpty ? .secondary : Color.jellyfinPurple)
}
.disabled(viewModel.isLoading || username.isEmpty)
.buttonStyle(.plain)
Button {
presentQuickConnect = true
} label: {
L10n.quickConnect.text
.frame(height: 75)
.frame(maxWidth: .infinity)
.background(Color.secondary)
}
.buttonStyle(.plain)
} header: {
L10n.signInToServer(viewModel.server.name).text
}
}
}
@ViewBuilder
private var publicUsersGrid: some View {
VStack {
L10n.publicUsers.text
.font(.title3)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
if viewModel.publicUsers.isEmpty {
L10n.noPublicUsers.text
.font(.callout)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.offset(y: -50)
} else {
CollectionView(items: viewModel.publicUsers) { _, user, _ in
UserProfileButton(user: user)
.onSelect {
username = user.name ?? ""
focusedField = .password
}
}
.layout { _, layoutEnvironment in
.grid(
layoutEnvironment: layoutEnvironment,
layoutMode: .adaptive(withMinItemSize: 250),
itemSpacing: 20,
lineSpacing: 20,
sectionInsets: .init(top: 20, leading: 20, bottom: 20, trailing: 20)
)
}
}
}
}
@ViewBuilder
private var quickConnect: some View {
ZStack {
BlurView()
.ignoresSafeArea()
VStack(alignment: .center) {
L10n.quickConnect.text
.font(.title3)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 20) {
L10n.quickConnectStep1.text
L10n.quickConnectStep2.text
L10n.quickConnectStep3.text
}
.padding(.vertical)
Text(viewModel.quickConnectCode ?? "------")
.tracking(10)
.font(.title)
.monospacedDigit()
.frame(maxWidth: .infinity)
Button {
presentQuickConnect = false
} label: {
L10n.close.text
.frame(width: 400, height: 75)
}
.buttonStyle(.plain)
}
}
.onAppear {
viewModel.startQuickConnect {}
}
.onDisappear {
viewModel.stopQuickConnectAuthCheck()
}
}
var body: some View {
ZStack {
@ -29,88 +167,24 @@ struct UserSignInView: View {
.ignoresSafeArea()
HStack(alignment: .top) {
VStack(alignment: .leading) {
Section {
TextField(L10n.username, text: $username)
.disableAutocorrection(true)
.autocapitalization(.none)
SecureField(L10n.password, text: $password)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
viewModel.signIn(username: username, password: password)
} label: {
HStack {
L10n.connect.text
Spacer()
if viewModel.isLoading {
ProgressView()
}
}
}
.disabled(viewModel.isLoading || username.isEmpty)
} header: {
L10n.signInToServer(viewModel.server.name).text
.foregroundColor(.secondary)
}
Spacer()
if !viewModel.quickConnectEnabled {
L10n.quickConnectNotEnabled.text
}
}
.frame(maxWidth: .infinity)
.alert(item: $viewModel.errorMessage) { _ in
Alert(
title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()
)
}
.navigationTitle(L10n.signIn)
if viewModel.quickConnectEnabled {
VStack(alignment: .center) {
L10n.quickConnect.text
.font(.title3)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 20) {
L10n.quickConnectStep1.text
L10n.quickConnectStep2.text
L10n.quickConnectStep3.text
}
.padding(.vertical)
Text(viewModel.quickConnectCode ?? "------")
.tracking(10)
.font(.title)
.monospacedDigit()
.frame(maxWidth: .infinity)
}
signInForm
.frame(maxWidth: .infinity)
publicUsersGrid
.frame(maxWidth: .infinity)
.onAppear {
viewModel.startQuickConnect {}
}
.onDisappear {
viewModel.stopQuickConnectAuthCheck()
}
}
}
.edgesIgnoringSafeArea(.bottom)
}
.navigationTitle(L10n.signIn)
.alert(item: $viewModel.errorMessage) { _ in
Alert(
title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()
)
}
.fullScreenCover(isPresented: $presentQuickConnect, onDismiss: nil) {
quickConnect
}
}
}
// struct UserSignInView_Preivews: PreviewProvider {
// static var previews: some View {
// UserSignInView(viewModel: .init(server: .sample))
// }
// }

View File

@ -97,7 +97,7 @@ struct SmallMediaStreamSelectionView: View {
.padding()
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.background(Color.clear)
.focused($focusedLayer, equals: .subtitles)
.focused($subtitlesFocused)
@ -129,7 +129,7 @@ struct SmallMediaStreamSelectionView: View {
.padding()
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.background(Color.clear)
.focused($focusedLayer, equals: .audio)
.focused($audioFocused)
@ -161,7 +161,7 @@ struct SmallMediaStreamSelectionView: View {
.padding()
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.background(Color.clear)
.focused($focusedLayer, equals: .playbackSpeed)
.focused($playbackSpeedFocused)
@ -194,7 +194,7 @@ struct SmallMediaStreamSelectionView: View {
.padding()
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.background(Color.clear)
.focused($focusedLayer, equals: .chapters)
.focused($chaptersFocused)
@ -335,7 +335,7 @@ struct SmallMediaStreamSelectionView: View {
.cornerRadius(10)
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.buttonStyle(.card)
VStack(alignment: .leading, spacing: 5) {

View File

@ -476,6 +476,7 @@
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92617288756BD002A7A66 /* PosterButton.swift */; };
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92618288756BD002A7A66 /* DotHStack.swift */; };
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C92619288756BD002A7A66 /* PosterHStack.swift */; };
E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */; };
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; };
E1CCF12F28ABF989006CAC9E /* PosterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF12D28ABF989006CAC9E /* PosterType.swift */; };
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; };
@ -509,6 +510,8 @@
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; };
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */; };
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */; };
E1E9EFEA28C6B96500CC1F8B /* ServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */; };
E1E9EFEB28C7EA2C00CC1F8B /* UserDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */; };
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */; };
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; };
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; };
@ -921,6 +924,7 @@
E1C92617288756BD002A7A66 /* PosterButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PosterButton.swift; sourceTree = "<group>"; };
E1C92618288756BD002A7A66 /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = "<group>"; };
E1C92619288756BD002A7A66 /* PosterHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileButton.swift; sourceTree = "<group>"; };
E1CCF12D28ABF989006CAC9E /* PosterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterType.swift; sourceTree = "<group>"; };
E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = "<group>"; };
E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
@ -944,6 +948,7 @@
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; };
E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmCloseOverlay.swift; sourceTree = "<group>"; };
E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerButton.swift; sourceTree = "<group>"; };
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedTextView.swift; sourceTree = "<group>"; };
E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewCoordinator.swift; sourceTree = "<group>"; };
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = "<group>"; };
@ -1217,10 +1222,12 @@
E1C92618288756BD002A7A66 /* DotHStack.swift */,
E103A6A1278A7EB500820EC7 /* HomeCinematicView */,
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */,
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
E1C92617288756BD002A7A66 /* PosterButton.swift */,
E1C92619288756BD002A7A66 /* PosterHStack.swift */,
536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */,
E17885A3278105170094FBCF /* SFSymbolButton.swift */,
);
path = Components;
@ -2455,6 +2462,7 @@
buildActionMask = 2147483647;
files = (
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */,
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */,
E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
@ -2486,6 +2494,7 @@
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */,
E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */,
E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */,
E1E9EFEB28C7EA2C00CC1F8B /* UserDtoExtensions.swift in Sources */,
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */,
E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */,
@ -2618,6 +2627,7 @@
E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */,
6264E88D273850380081A12A /* Strings.swift in Sources */,
E1C926102887565C002A7A66 /* PlayButton.swift in Sources */,
E1E9EFEA28C6B96500CC1F8B /* ServerButton.swift in Sources */,
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,

View File

@ -43,9 +43,17 @@ struct ConnectToServerView: View {
Button {
viewModel.connectToServer(uri: uri)
} label: {
L10n.connect.text
HStack {
L10n.connect.text
Spacer()
if viewModel.isLoading {
ProgressView()
}
}
}
.disabled(uri.isEmpty)
.disabled(uri.isEmpty || viewModel.isLoading)
}
} header: {
L10n.connectToJellyfinServer.text
@ -69,15 +77,15 @@ struct ConnectToServerView: View {
Spacer()
}
} else {
ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in
ForEach(viewModel.discoveredServers, id: \.id) { server in
Button {
uri = discoveredServer.url.absoluteString
viewModel.connectToServer(uri: discoveredServer.url.absoluteString)
uri = server.currentURI
viewModel.connectToServer(uri: server.currentURI)
} label: {
VStack(alignment: .leading, spacing: 5) {
Text(discoveredServer.name)
Text(server.name)
.font(.title3)
Text(discoveredServer.host)
Text(server.currentURI)
.font(.subheadline)
.foregroundColor(.secondary)
}

View File

@ -68,7 +68,7 @@ extension ItemView {
}
.frame(width: 330, height: 195)
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
}
.padding(.horizontal)
.if(UIDevice.isIPad) { view in

View File

@ -39,7 +39,7 @@ extension ItemView {
// .foregroundStyle(.white)
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.if(equalSpacing) { view in
view.frame(maxWidth: .infinity)
}
@ -57,7 +57,7 @@ extension ItemView {
// .foregroundStyle(.white)
}
}
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.if(equalSpacing) { view in
view.frame(maxWidth: .infinity)
}