tvOS - Revamp Connect Flow (#563)
This commit is contained in:
parent
d1414f2855
commit
859a47803f
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(_:)))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ struct CinematicNextUpCardView: View {
|
|||
}
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.buttonStyle(.card)
|
||||
.padding(.top)
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
|
|
@ -63,7 +63,7 @@ struct CinematicResumeCardView: View {
|
|||
}
|
||||
.frame(width: 350, height: 210)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.buttonStyle(.card)
|
||||
.padding(.top)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ struct ContinueWatchingCard: View {
|
|||
}
|
||||
.frame(width: 500, height: 281.25)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.buttonStyle(.card)
|
||||
.padding(.top)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
|
|
@ -37,7 +37,7 @@ extension ItemView.AboutView {
|
|||
.padding2()
|
||||
.frame(width: 700, height: 405)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.buttonStyle(.card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ extension SeriesEpisodesView {
|
|||
.foregroundColor(.black)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(.plain)
|
||||
.id(season)
|
||||
.focused($focusedSeason, equals: season)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ struct LatestInLibraryView: View {
|
|||
}
|
||||
.posterStyle(type: .portrait, width: 250)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.onSelect { item in
|
||||
router.route(to: \.item, item)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ extension ItemView {
|
|||
}
|
||||
.frame(width: 330, height: 195)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isIPad) { view in
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue