From a5e6cdf9988ffafa89fc09d8ef0f98b91bf6fe9f Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 14 Oct 2021 14:17:50 -0600 Subject: [PATCH] Add better views and flow for no servers/users --- JellyfinPlayer/App/EmailHelper.swift | 6 +- JellyfinPlayer/App/JellyfinPlayerApp.swift | 31 ++--- JellyfinPlayer/Views/ServerListView.swift | 143 +++++++++++++++----- JellyfinPlayer/Views/UserListView.swift | 108 ++++++++++++--- Shared/ViewModels/ServerListViewModel.swift | 8 ++ 5 files changed, 217 insertions(+), 79 deletions(-) diff --git a/JellyfinPlayer/App/EmailHelper.swift b/JellyfinPlayer/App/EmailHelper.swift index c5025ac1..5c054d9f 100644 --- a/JellyfinPlayer/App/EmailHelper.swift +++ b/JellyfinPlayer/App/EmailHelper.swift @@ -11,10 +11,10 @@ import SwiftUI import MessageUI class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { + public static let shared = EmailHelper() - override private init() { - // - } + + override private init() { } func sendLogs(logURL: URL) { if !MFMailComposeViewController.canSendMail() { diff --git a/JellyfinPlayer/App/JellyfinPlayerApp.swift b/JellyfinPlayer/App/JellyfinPlayerApp.swift index 2e05a2b3..99e0f929 100644 --- a/JellyfinPlayer/App/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/App/JellyfinPlayerApp.swift @@ -148,31 +148,20 @@ struct JellyfinPlayerApp: App { var body: some Scene { WindowGroup { - MainCoordinator().view() + EmptyView() .onAppear { setupAppearance() } + .withHostingWindow { window in + window?.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view()) + } + .onShake { + EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) + } + .onOpenURL { url in + AppURLHandler.shared.processDeepLink(url: url) + } } - - -// WindowGroup { -// EmptyView() -// .environment(\.managedObjectContext, persistenceController.container.viewContext) -// .onAppear(perform: { -// setupAppearance() -// }) -// .withHostingWindow { window in -// window? -// .rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view() -// .environment(\.managedObjectContext, persistenceController.container.viewContext)) -// } -// .onShake { -// EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) -// } -// .onOpenURL { url in -// AppURLHandler.shared.processDeepLink(url: url) -// } -// } } private func setupAppearance() { diff --git a/JellyfinPlayer/Views/ServerListView.swift b/JellyfinPlayer/Views/ServerListView.swift index 883f1c7f..73fd01de 100644 --- a/JellyfinPlayer/Views/ServerListView.swift +++ b/JellyfinPlayer/Views/ServerListView.swift @@ -15,47 +15,120 @@ struct ServerListView: View { @EnvironmentObject var serverListRouter: ServerListCoordinator.Router @ObservedObject var viewModel: ServerListViewModel - var body: some View { - List { - ForEach(viewModel.servers, id: \.id) { server in - Button { - serverListRouter.route(to: \.userList, server) - } label: { - Text(server.name) + @ViewBuilder + private var listView: some View { + ScrollView { + VStack { + ForEach(viewModel.servers, id: \.id) { server in + Button { + serverListRouter.route(to: \.userList, server) + } label: { + ZStack(alignment: Alignment.leading) { + Rectangle() + .foregroundColor(Color(UIColor.secondarySystemFill)) + .frame(height: 100) + .cornerRadius(10) + + HStack { + Image(systemName: "server.rack") + .font(.system(size: 36)) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 5) { + Text(server.name) + .font(.title2) + .foregroundColor(.primary) + + Text(server.uri) + .font(.footnote) + .disabled(true) + .foregroundColor(.secondary) + + Text(viewModel.userTextFor(server: server)) + .font(.footnote) + .foregroundColor(.primary) + } + }.padding([.leading]) + } + .padding() + } } } } - .navigationTitle("Servers") - .toolbar { - ToolbarItemGroup(placement: .navigation) { - HStack { - Button { - serverListRouter.route(to: \.connectToServer) - } label: { - Text("Connect") - } + } + + @ViewBuilder + private var noServerView: some View { + VStack { + Text("Connect to a Jellyfin server to get started.") + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + ZStack { + Rectangle() + .foregroundColor(Color.jellyfinPurple) + .frame(maxWidth: 500, maxHeight: 50) + .frame(height: 50) + .cornerRadius(10) + .padding([.leading, .trailing], 30) + .padding([.top, .bottom], 20) - Button { - SwiftfinStore.dataStack.perform(asynchronous: { transaction in - try! transaction.deleteAll(From()) - try! transaction.deleteAll(From()) - try! transaction.deleteAll(From()) - }) { _ in - SwiftfinStore.Defaults.suite[.lastServerUserID] = nil - viewModel.fetchServers() - } - } label: { - Text("Purge") - } + Text("Connect") + .foregroundColor(Color.white) + .bold() } } -// ToolbarItem(placement: .navigationBarTrailing) { -// Button { -// serverListRouter.route(to: \.connectToServer) -// } label: { -// Text("Connect") -// } -// } + } + } + + @ViewBuilder + private var innerBody: some View { + if viewModel.servers.isEmpty { + noServerView + .offset(y: -50) + } else { + listView + } + } + + @ViewBuilder + private var toolbarContent: some View { + if viewModel.servers.isEmpty { + EmptyView() + } else { + HStack { + Button { + SwiftfinStore.dataStack.perform(asynchronous: { transaction in + try! transaction.deleteAll(From()) + try! transaction.deleteAll(From()) + try! transaction.deleteAll(From()) + }) { _ in + SwiftfinStore.Defaults.suite[.lastServerUserID] = nil + viewModel.fetchServers() + } + } label: { + Text("Purge") + } + + Button { + serverListRouter.route(to: \.connectToServer) + } label: { + Image(systemName: "plus.circle.fill") + } + } + } + } + + var body: some View { + innerBody + .navigationTitle("Servers") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + toolbarContent + } } .onAppear { viewModel.fetchServers() diff --git a/JellyfinPlayer/Views/UserListView.swift b/JellyfinPlayer/Views/UserListView.swift index 0a5cf488..e320768b 100644 --- a/JellyfinPlayer/Views/UserListView.swift +++ b/JellyfinPlayer/Views/UserListView.swift @@ -14,34 +14,102 @@ struct UserListView: View { @EnvironmentObject var userListRouter: UserListCoordinator.Router @ObservedObject var viewModel: UserListViewModel - var body: some View { - List { - ForEach(viewModel.users, id: \.id) { user in - Button { - viewModel.login(user: user) - } label: { - HStack { - Text(user.username) - Spacer() - if viewModel.isLoading { - ProgressView() + @ViewBuilder + private var listView: some View { + ScrollView { + VStack { + ForEach(viewModel.users, id: \.id) { user in + Button { + viewModel.login(user: user) + } label: { + ZStack(alignment: Alignment.leading) { + Rectangle() + .foregroundColor(Color(UIColor.secondarySystemFill)) + .frame(height: 70) + .cornerRadius(10) + + HStack { + Image(systemName: "person.crop.circle.fill") + .font(.system(size: 46)) + .foregroundColor(.primary) + + Text(user.username) + .font(.title2) + + Spacer() + + if viewModel.isLoading { + ProgressView() + } + }.padding(.leading) } + .padding() } } } } - .navigationTitle("Users") - .toolbar { - ToolbarItem(placement: .navigation) { - HStack { - Button { - userListRouter.route(to: \.userLogin, viewModel.server) - } label: { - Text("Connect") - } + } + + @ViewBuilder + private var noUserView: some View { + VStack { + Text("Login to a user to get started.") + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + Button { + userListRouter.route(to: \.userLogin, viewModel.server) + } label: { + ZStack { + Rectangle() + .foregroundColor(Color.jellyfinPurple) + .frame(maxWidth: 500, maxHeight: 50) + .frame(height: 50) + .cornerRadius(10) + .padding([.leading, .trailing], 30) + .padding([.top, .bottom], 20) + + Text("Login") + .foregroundColor(Color.white) + .bold() } } } + } + + @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: \.userLogin, viewModel.server) + } label: { + Image(systemName: "person.crop.circle.fill.badge.plus") + } + } + } + } + + var body: some View { + innerBody + .navigationTitle(viewModel.server.name) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + toolbarContent + } + } .onAppear { viewModel.fetchUsers() } diff --git a/Shared/ViewModels/ServerListViewModel.swift b/Shared/ViewModels/ServerListViewModel.swift index 94293adf..9df10683 100644 --- a/Shared/ViewModels/ServerListViewModel.swift +++ b/Shared/ViewModels/ServerListViewModel.swift @@ -17,4 +17,12 @@ class ServerListViewModel: ObservableObject { func fetchServers() { self.servers = SessionManager.main.fetchServers() } + + func userTextFor(server: SwiftfinStore.State.Server) -> String { + if server.userIDs.count == 1 { + return "1 user" + } else { + return "\(server.userIDs.count) users" + } + } }