diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-19 20:38:13 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-19 20:38:13 -0800 |
| commit | 4fd69c02a3e7b581bb9229865336c315153f3b18 (patch) | |
| tree | e6e11d872424196f2b4033077902126ad5556e40 /Hotline/Views | |
| parent | 5e87b5927cd931d46fb5f72fb035480a95969a9f (diff) | |
Beginnings of a UI for macOS as well as some visual changes to iOS client. :)
Diffstat (limited to 'Hotline/Views')
| -rw-r--r-- | Hotline/Views/ChatView.swift | 153 | ||||
| -rw-r--r-- | Hotline/Views/FilesView.swift | 140 | ||||
| -rw-r--r-- | Hotline/Views/MessageBoardView.swift | 74 | ||||
| -rw-r--r-- | Hotline/Views/NewsView.swift | 205 | ||||
| -rw-r--r-- | Hotline/Views/ServerView.swift | 51 | ||||
| -rw-r--r-- | Hotline/Views/TrackerView.swift | 397 | ||||
| -rw-r--r-- | Hotline/Views/UsersView.swift | 41 |
7 files changed, 0 insertions, 1061 deletions
diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift deleted file mode 100644 index e8c7754..0000000 --- a/Hotline/Views/ChatView.swift +++ /dev/null @@ -1,153 +0,0 @@ -import SwiftUI - -extension View { - func endEditing() { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } -} - -struct ChatView: View { - @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme - - @State var input: String = "" - @State private var scrollPos: Int? - @State private var contentHeight: CGFloat = 0 - - @Namespace var bottomID - - var body: some View { - NavigationStack { - VStack(spacing: 0) { - ScrollViewReader { reader in - ScrollView { - LazyVStack(alignment: .leading) { - ForEach(model.chat) { msg in - if msg.type == .agreement { - VStack(alignment: .leading) { - VStack(alignment: .leading, spacing: 0) { - Text(msg.text) - .textSelection(.enabled) - .padding() - .opacity(0.75) - HStack { - Spacer() - Text((model.serverTitle) + " Server Agreement") - .font(.caption) - .fontWeight(.medium) - .opacity(0.4) - .lineLimit(1) - .truncationMode(.middle) - Spacer() - } - .padding() - .background(colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9)) - } - .background(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96)) - .cornerRadius(16) - .frame(maxWidth: .infinity) - } - .padding() - } - else if msg.type == .status { - HStack { - Spacer() - Text(msg.text) - .lineLimit(1) - .truncationMode(.middle) - .opacity(0.3) - Spacer() - } - .padding() - } - else { - HStack(alignment: .firstTextBaseline) { - if let username = msg.username { - Text("**\(username):** \(msg.text)") - } - else { - Text(msg.text) - .textSelection(.enabled) - } - Spacer() - } - .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) - } - } - EmptyView().id(bottomID) - } - .padding(.bottom, 12) - } - .defaultScrollAnchor(.bottom) - .onChange(of: model.chat.count) { - withAnimation { - reader.scrollTo(bottomID, anchor: .bottom) - } - print("SCROLLED TO BOTTOM") - } - .onAppear { - print("SCROLLED TO BOTTOM ON APPEAR") - reader.scrollTo(bottomID, anchor: .bottom) - } - .scrollDismissesKeyboard(.interactively) - .onTapGesture { - self.endEditing() - } - } - - Divider() - - HStack(alignment: .top) { - Image(systemName: "chevron.right").opacity(0.4) - TextField("", text: $input, axis: .vertical) - .autocapitalization(.none) - .lineLimit(1...5) - .onSubmit { - if !self.input.isEmpty { - model.sendChat(self.input) - // hotline.sendChat(message: self.input) - } - self.input = "" - } - .frame(maxWidth: .infinity) - Button { - if !self.input.isEmpty { - model.sendChat(self.input) - // hotline.sendChat(message: self.input) - } - self.input = "" - } label: { - Image(systemName: self.input.isEmpty ? "arrow.up.circle" : "arrow.up.circle.fill") - .resizable() - .scaledToFit() - .frame(width: 24.0, height: 24.0) - .opacity(self.input.isEmpty ? 0.4 : 1.0) - } - }.padding() - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(model.serverTitle) - .font(.headline) - } - ToolbarItem(placement: .navigationBarLeading) { - Button { - model.disconnect() - } label: { - Text(Image(systemName: "xmark.circle.fill")) - .symbolRenderingMode(.hierarchical) - .font(.title2) - .foregroundColor(.secondary) - } - } - } - - } - } -} - -#Preview { - ChatView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift deleted file mode 100644 index 8ad3837..0000000 --- a/Hotline/Views/FilesView.swift +++ /dev/null @@ -1,140 +0,0 @@ -import SwiftUI -import UniformTypeIdentifiers - -struct FileView: View { - @Environment(Hotline.self) private var model: Hotline - - @State var expanded = false - - var file: FileInfo - - var body: some View { - if file.isFolder { - DisclosureGroup(isExpanded: $expanded) { - ForEach(file.children!) { childFile in - FileView(file: childFile) - .frame(height: 44) - } - } label: { - HStack { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Image(systemName: "folder.fill") - } - .frame(minWidth: 25) - Text(file.name).fontWeight(.medium).lineLimit(1).truncationMode(.tail) - Spacer() - Text("\(file.fileSize)").foregroundStyle(.secondary).lineLimit(1) - } - } - .onChange(of: expanded) { - if !expanded { - return - } - - Task { - await model.getFileList(path: file.path) - } - } - } - else { - HStack { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Image(uiImage: fileIcon(name: file.name)) - .renderingMode(.template) - } - .frame(minWidth: 25) - Text(file.name).lineLimit(1).truncationMode(.tail) - Spacer() - Text(formattedFileSize(file.fileSize)).foregroundStyle(.secondary).lineLimit(1) - } - } - } - - static let byteFormatter = ByteCountFormatter() - - private func formattedFileSize(_ fileSize: UInt) -> String { - // let bcf = ByteCountFormatter() - FileView.byteFormatter.allowedUnits = [.useAll] - FileView.byteFormatter.countStyle = .file - return FileView.byteFormatter.string(fromByteCount: Int64(fileSize)) - } - - private func fileIcon(name: String) -> UIImage { - // func utTypeForFilename(_ filename: String) -> UTType? { - let fileExtension = (name as NSString).pathExtension - if let fileType = UTType(filenameExtension: fileExtension) { - print("\(name) \(fileExtension) = \(fileType)") - - if fileType.isSubtype(of: .movie) { - return UIImage(systemName: "play.rectangle")! - } - else if fileType.isSubtype(of: .image) { - return UIImage(systemName: "photo")! - } - else if fileType.isSubtype(of: .archive) { - return UIImage(systemName: "doc.zipper")! - } - else if fileType.isSubtype(of: .text) { - return UIImage(systemName: "doc.text")! - } - else { - return UIImage(systemName: "doc")! - } - } - - return UIImage(systemName: "doc")! - } -} - -struct FilesView: View { - @Environment(Hotline.self) private var model: Hotline - - @State var initialLoadComplete = false - - var body: some View { - NavigationStack { - List(model.files) { file in - FileView(file: file) - .frame(height: 44) - } - .task { - if !initialLoadComplete { - let _ = await model.getFileList() - initialLoadComplete = true - } - } - .overlay { - if !initialLoadComplete { - VStack { - ProgressView() - .controlSize(.large) - } - .frame(maxWidth: .infinity) - } - } - .listStyle(.plain) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(model.serverTitle) - .font(.headline) - } - ToolbarItem(placement: .navigationBarLeading) { - Button { - model.disconnect() - } label: { - Text(Image(systemName: "xmark.circle.fill")) - .symbolRenderingMode(.hierarchical) - .font(.title2) - .foregroundColor(.secondary) - } - } - } - } - } -} - -#Preview { - FilesView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift deleted file mode 100644 index 0d3968f..0000000 --- a/Hotline/Views/MessageBoardView.swift +++ /dev/null @@ -1,74 +0,0 @@ -import SwiftUI - -struct MessageBoardView: View { - @Environment(Hotline.self) private var model: Hotline - - @State private var initialLoadComplete = false - @State private var fetched = false - - var body: some View { - NavigationStack { - ScrollView { - LazyVStack(alignment: .leading) { - ForEach(model.messageBoard, id: \.self) { - Text($0) - .lineLimit(100) - .padding() - .textSelection(.enabled) - Divider() - } - } - Spacer() - } - .task { - if !initialLoadComplete { - let _ = await model.getMessageBoard() - initialLoadComplete = true - } - } - .overlay { - if !initialLoadComplete { - VStack { - ProgressView() - .controlSize(.large) - } - .frame(maxWidth: .infinity) - } - } - .refreshable { - let _ = await model.getMessageBoard() - initialLoadComplete = true - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(model.serverTitle) - .font(.headline) - } - ToolbarItem(placement: .navigationBarLeading) { - Button { - model.disconnect() - } label: { - Text(Image(systemName: "xmark.circle.fill")) - .symbolRenderingMode(.hierarchical) - .font(.title2) - .foregroundColor(.secondary) - } - } - ToolbarItem(placement: .navigationBarTrailing) { - Button { - - } label: { - Image(systemName: "square.and.pencil") - } - } - } - } - - } -} - -#Preview { - MessageBoardView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift deleted file mode 100644 index bd3ae1c..0000000 --- a/Hotline/Views/NewsView.swift +++ /dev/null @@ -1,205 +0,0 @@ -import SwiftUI -import UniformTypeIdentifiers - - - -struct NewsItemView: View { - @Environment(Hotline.self) private var model: Hotline - @Environment(NewsItemSelection.self) private var selectedArticle: NewsItemSelection - - let news: NewsInfo - - @State var expanded = false - - var body: some View { - if news.count > 0 { - DisclosureGroup(isExpanded: $expanded) { - ForEach(news.children) { childNews in - NewsItemView(news: childNews) - .environment(self.selectedArticle) - .frame(height: 38) - } - } label: { - HStack { - if news.type == .bundle { - Text(Image(systemName: "tray.2.fill")) - } - else { - Text(Image(systemName: "tray.full.fill")) - } - Text(news.name) - .fontWeight(.medium) - .lineLimit(1) - .truncationMode(.tail) - Spacer() - if news.count > 0 { - Text("\(news.count)") - .foregroundStyle(.secondary) - } - } - } - .onChange(of: expanded) { - if !expanded { - return - } - - Task { - await model.getNewsList(at: news.path) - } - } - } - else { - HStack { - Text(Image(systemName: "doc.text")) - Text(news.name) - .fontWeight(.medium) - .lineLimit(1) - .truncationMode(.tail) - Spacer() - if news.count > 0 { - Text("\(news.count)") - .foregroundStyle(.secondary) - } - } - .onTapGesture { - if news.type == .article { - print("SELECTED", news.name) - selectedArticle.selectedArticle = news - } - } - } - } -} - -@Observable -class NewsItemSelection: Equatable { - var selectedArticle: NewsInfo? = nil - - static func == (lhs: NewsItemSelection, rhs: NewsItemSelection) -> Bool { - return lhs.selectedArticle == rhs.selectedArticle - } -} - -struct NewsView: View { - @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme - - @State private var fetched = false - @State private var selectedCategory: NewsInfo? = nil - @State private var topListHeight: CGFloat = 200 - @State private var dividerHeight: CGFloat = 30 - - @State private var articleSelection = NewsItemSelection() - @State private var articleText = "" -// @State private var selectedArticleID: UInt? - - var articleList: some View { - VStack(spacing: 0) { - if model.news.count == 0 { - Text("No News Available") - .font(.headline) - .opacity(0.3) - } - else { - List(model.news) { category in - NewsItemView(news: category) - .environment(self.articleSelection) - .frame(height: 38) - } - .scrollBounceBehavior(.basedOnSize) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(uiColor: .systemGroupedBackground)) - - // .listStyle(.plain) - } - - var body: some View { - NavigationStack { - VStack(spacing: 0) { - articleList - .frame(height: topListHeight) - .frame(minHeight: topListHeight) - .onChange(of: self.articleSelection.selectedArticle) { - self.articleText = "" - if - let article = self.articleSelection.selectedArticle, - let articleFlavor = article.articleFlavors?.first, - let articleID = article.articleID { - Task { - if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) { - self.articleText = articleText - } - } - } -// print("SELECTED ARTICLE", articleSelection.selectedArticle?.name) - } - - // Movable Divider - VStack(alignment: .center) { - Divider() - Spacer() - HStack(alignment: .center) { - Rectangle() - .fill(.tertiary) - .frame(width: 50, height: 6, alignment: .center) - .cornerRadius(10) - } - Spacer() - } - .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground)) - .frame(maxWidth: .infinity) - .frame(height: dividerHeight) - .gesture( - DragGesture() - .onChanged { gesture in - let delta = gesture.translation.height - topListHeight = max(min(topListHeight + delta, 500), 50) - } - ) - - // Reader View - ScrollView(.vertical) { - HStack(alignment: .top, spacing: 0) { - Text(self.articleText) - .multilineTextAlignment(.leading) - Spacer() - } - .padding() - } - .scrollBounceBehavior(.basedOnSize) - .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground)) - } - .task { - if !fetched { - let _ = await model.getNewsList() - fetched = true - } - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(model.serverTitle) - .font(.headline) - } - ToolbarItem(placement: .navigationBarLeading) { - Button { - model.disconnect() - } label: { - Text(Image(systemName: "xmark.circle.fill")) - .symbolRenderingMode(.hierarchical) - .font(.title2) - .foregroundColor(.secondary) - } - } - } - } - } -} - -#Preview { - MessageBoardView() - .environment(HotlineState()) - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} diff --git a/Hotline/Views/ServerView.swift b/Hotline/Views/ServerView.swift deleted file mode 100644 index 09ab877..0000000 --- a/Hotline/Views/ServerView.swift +++ /dev/null @@ -1,51 +0,0 @@ -import SwiftUI - -struct ServerView: View { - @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme - - enum Tab { - case chat, users, news, messageBoard, files - } - - var body: some View { - TabView { - ChatView() - .tabItem { - Image(systemName: "bubble") - } - .tag(Tab.chat) - - UsersView() - .tabItem { - Image(systemName: "person.2") - } - .tag(Tab.users) - - if let v = model.serverVersion, v >= 150 { - NewsView() - .tabItem { - Image(systemName: "newspaper") - } - .tag(Tab.news) - } - - MessageBoardView() - .tabItem { - Image(systemName: "book.closed") - } - .tag(Tab.messageBoard) - - FilesView() - .tabItem { - Image(systemName: "folder").tint(.black) - } - .tag(Tab.files) - } - .accentColor(colorScheme == .dark ? .white : .black) - } -} - -#Preview { - ServerView() -} diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift deleted file mode 100644 index 6204fd2..0000000 --- a/Hotline/Views/TrackerView.swift +++ /dev/null @@ -1,397 +0,0 @@ -import SwiftUI - -struct TrackerConnectView: View { - @Environment(Hotline.self) private var model: Hotline - @Environment(\.dismiss) var dismiss - @Environment(\.colorScheme) var colorScheme - - @State private var server: Server? - @State private var address = "" - @State private var login = "" - @State private var password = "" - @State private var connecting = false - - func connectionStatusToProgress(status: HotlineClientStatus) -> Double { - switch status { - case .disconnected: - return 0.0 - case .connecting: - return 0.1 - case .connected: - return 0.25 - case .loggingIn: - return 0.5 - case .loggedIn: - return 1.0 - } - } - - var body: some View { - VStack(alignment: .leading) { - if !connecting { - TextField("Server Address", text: $address) - .keyboardType(.URL) - .disableAutocorrection(true) - .frame(height: 48) - .textFieldStyle(.plain) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - .background { - Color.black.cornerRadius(8).blendMode(.overlay) - } - TextField("Login", text: $login) - .disableAutocorrection(true) - .frame(height: 48) - .textFieldStyle(.plain) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - .background { - Color.black.cornerRadius(8).blendMode(.overlay) - } - SecureField("Password", text: $password) - .disableAutocorrection(true) - .textFieldStyle(.plain) - .frame(height: 48) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - .background { - Color.black.cornerRadius(8).blendMode(.overlay) - } - } - else { - ProgressView(value: connectionStatusToProgress(status: model.status)) - .frame(minHeight: 10) - .accentColor(colorScheme == .dark ? .white : .black) - } - - Spacer() - - HStack { - Button { - dismiss() - server = nil - model.disconnect() - } label: { - Text("Cancel") - } - .bold() - .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) - .frame(maxWidth: .infinity) - .foregroundColor(colorScheme == .dark ? .white : .black) - .background( - colorScheme == .dark ? - LinearGradient(gradient: Gradient(colors: [Color(white: 0.4), Color(white: 0.3)]), startPoint: .top, endPoint: .bottom) - : - LinearGradient(gradient: Gradient(colors: [Color(white: 0.95), Color(white: 0.91)]), startPoint: .top, endPoint: .bottom) - ) - .overlay( - RoundedRectangle(cornerRadius: 10.0).stroke(.black, lineWidth: 3).opacity(colorScheme == .dark ? 0.0 : 0.2) - ) - .cornerRadius(10.0) - Button { - let s = Server(name: nil, description: nil, address: address, port: Server.defaultPort, users: 0) - server = s - connecting = true - Task { - let loggedIn = await model.login(server: s, login: login, password: password, username: "bolt", iconID: 128) - if !loggedIn { - connecting = false - } - } - } label: { - Text("Connect") - } - .disabled(connecting) - .bold() - .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) - .frame(maxWidth: .infinity) - .foregroundColor(colorScheme == .dark ? .white : .black) - .background( - colorScheme == .dark ? - LinearGradient(gradient: Gradient(colors: [Color(white: 0.4), Color(white: 0.3)]), startPoint: .top, endPoint: .bottom) - : - LinearGradient(gradient: Gradient(colors: [Color(white: 0.95), Color(white: 0.91)]), startPoint: .top, endPoint: .bottom) - ) - .overlay( - RoundedRectangle(cornerRadius: 10.0).stroke(.black, lineWidth: 3).opacity(colorScheme == .dark ? 0.0 : 0.2) - ) - .cornerRadius(10.0) - } - .padding() - } - .padding() - .onChange(of: model.status) { - print("MODEL STATUS CHANGED") - if model.server != nil && server != nil && model.server! == server! { - if model.status == .loggedIn { - dismiss() - } - else { - connecting = (model.status != .disconnected) - } - } - } - .toolbar { - ToolbarItem(placement: .principal) { - Text("Connect to Server") - .font(.headline) - } - } -// .presentationBackground(.regularMaterial, in: Color(uiColor: .systemGroupedBackground)) - .presentationBackground { - Color.clear - .background(Material.regular) - } - .presentationDetents([.height(300), .large]) - .presentationDragIndicator(.visible) - .presentationCornerRadius(20) - // .background(Color(uiColor: .systemGroupedBackground)) - } -} - -struct TrackerView: View { - - // @Environment(\.modelContext) private var modelContext - // @Query private var items: [Item] - - @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme - - // @State private var tracker = Tracker(address: "hltracker.com", service: trackerService) - - @State private var servers: [Server] = [] - @State private var selectedServer: Server? - @State private var scrollOffset: CGFloat = CGFloat.zero - @State private var initialLoadComplete = false - @State private var refreshing = false - @State private var topBarOpacity: Double = 1.0 - @State private var connectVisible = false - @State private var connectDismissed = true - @State private var serverVisible = false - - func shouldDisplayDescription(server: Server) -> Bool { - guard let desc = server.description else { - return false - } - - return desc.count > 0 && desc != server.name - } - - func connectionStatusToProgress(status: HotlineClientStatus) -> Double { - switch status { - case .disconnected: - return 0.0 - case .connecting: - return 0.1 - case .connected: - return 0.25 - case .loggingIn: - return 0.5 - case .loggedIn: - return 1.0 - } - } - - func inverseLerp(lower: Double, upper: Double, v: Double) -> Double { - return (v - lower) / (upper - lower) - } - - func updateServers() async { -// "hltracker.com" -// "tracker.preterhuman.net" -// "hotline.ubersoft.org" -// "tracked.nailbat.com" -// "hotline.duckdns.org" -// "tracked.agent79.org" - self.servers = await model.getServerList(tracker: "hltracker.com") - } - - var body: some View { - ZStack(alignment: .center) { - VStack(alignment: .center) { - ZStack(alignment: .top) { - HStack(alignment: .center) { - Button { - connectVisible = true - connectDismissed = false - } label: { - Text(Image(systemName: "gearshape.fill")) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.primary) - .font(.title2) - .padding(.leading, 16) - } - .sheet(isPresented: $connectVisible) { - connectDismissed = true - } content: { - TrackerConnectView() - } - Spacer() - } - .frame(height: 40.0) - Image("Hotline") - .resizable() - .renderingMode(.template) - .foregroundColor(Color(hex: 0xE10000)) - .scaledToFit() - .frame(width: 40.0, height: 40.0) - HStack(alignment: .center) { - Spacer() - Button { - connectVisible = true - connectDismissed = false - } label: { - Text(Image(systemName: "point.3.connected.trianglepath.dotted")) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.primary) - .font(.title2) - .padding(.trailing, 16) - } - .sheet(isPresented: $connectVisible) { - connectDismissed = true - } content: { - TrackerConnectView() - } - } - .frame(height: 40.0) - } - .padding() - - Spacer() - } - .opacity(inverseLerp(lower: -50, upper: 0, v: scrollOffset)) - .opacity(scrollOffset > 65 ? 0.0 : 1.0) - .opacity(topBarOpacity) - .zIndex(scrollOffset > 0 ? 1 : 3) - ObservableScrollView(scrollOffset: $scrollOffset) { - LazyVStack(alignment: .leading) { - ForEach(self.servers) { server in - VStack(alignment: .leading) { - HStack(alignment: .firstTextBaseline) { - Image(systemName: "globe.americas.fill").font(.title3) - VStack(alignment: .leading) { - Text(server.name ?? server.address).font(.title3).fontWeight(.medium) - if shouldDisplayDescription(server: server) { - Spacer() - Text(server.description!).opacity(0.5).font(.system(size: 16)) - } - Spacer() - Text("\(server.address):" + String(format: "%i", server.port)).opacity(0.3).font(.system(size: 13)) - } - Spacer() - if server.users > 0 { - Text("\(server.users)").opacity(0.3).font(.system(size: 16)).fontWeight(.medium) - } - } - if server == selectedServer { - Spacer(minLength: 16) - - if model.status != .disconnected && model.server != nil && model.server! == server { - ProgressView(value: connectionStatusToProgress(status: model.status)) - .frame(minHeight: 10) - .accentColor(colorScheme == .dark ? .white : .black) - } - else { - Button("Connect") { - Task { - await model.login(server: server, login: "", password: "", username: "bolt", iconID: 128) - } - } - .bold() - .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) - .frame(maxWidth: .infinity) - .foregroundColor(colorScheme == .dark ? .white : .black) - .background( - colorScheme == .dark ? - LinearGradient(gradient: Gradient(colors: [Color(white: 0.4), Color(white: 0.3)]), startPoint: .top, endPoint: .bottom) - : - LinearGradient(gradient: Gradient(colors: [Color(white: 0.95), Color(white: 0.91)]), startPoint: .top, endPoint: .bottom) - ) - .overlay( - RoundedRectangle(cornerRadius: 10.0).stroke(.black, lineWidth: 3).opacity(colorScheme == .dark ? 0.0 : 0.2) - ) - .cornerRadius(10.0) - } - } - } - .multilineTextAlignment(.leading) - .padding() - .background(colorScheme == .dark ? Color(white: 0.12) : .white) - .cornerRadius(16) - .shadow(color: Color(white: 0.0, opacity: 0.1), radius: 16, x: 0, y: 10) - .onTapGesture { - withAnimation(.bouncy(duration: 0.25, extraBounce: 0.2)) { - selectedServer = server - } - } - } - .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) - } - .padding(EdgeInsets(top: 75, leading: 0, bottom: 0, trailing: 0)) - } - .zIndex(2) - .overlay { - if !initialLoadComplete { - VStack { - ProgressView() - .controlSize(.large) - } - .frame(maxWidth: .infinity) - } - } - .refreshable { - DispatchQueue.main.async { - withAnimation(.easeOut(duration: 0.1)) { - topBarOpacity = 0.0 - } - initialLoadComplete = true - } - - model.disconnectTracker() - await updateServers() - - DispatchQueue.main.async { - withAnimation(.easeOut(duration: 1.0).delay(0.75)) { - topBarOpacity = 1.0 - } - } - } - - - } - .fullScreenCover(isPresented: Binding(get: { return (connectDismissed && serverVisible) }, set: { _ in }), onDismiss: { - model.disconnect() - }) { - ServerView() - } - .onChange(of: model.status) { - serverVisible = (model.status == .loggedIn) - } - .background(Color(uiColor: UIColor.systemGroupedBackground)) - .frame(maxWidth: .infinity) - .task { - await updateServers() - initialLoadComplete = true - } - .onOpenURL(perform: { url in - guard url.scheme == "hotline" else { - return - } - - if let address = url.host() { - let login = url.user(percentEncoded: false) ?? "" - let password = url.password(percentEncoded: false) ?? "" - let port = url.port ?? Server.defaultPort - - Task { - model.disconnect() - let _ = await model.login(server: Server(name: nil, description: nil, address: address, port: port, users: 0), login: login, password: password, username: "bolt", iconID: 128) - } - - // TODO: Find a better way to show login status when trying to connect outside of the Tracker server list. Perhaps this opens the connect sheet prefilled. - } - }) - } -} - -#Preview { - TrackerView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} diff --git a/Hotline/Views/UsersView.swift b/Hotline/Views/UsersView.swift deleted file mode 100644 index d1f3318..0000000 --- a/Hotline/Views/UsersView.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftUI - -struct UsersView: View { - @Environment(Hotline.self) private var model: Hotline - - var body: some View { - NavigationStack { - List(model.users) { u in - Text("🤖 \(u.name)") - .fontWeight(.medium) - .lineLimit(1) - .truncationMode(.tail) - .foregroundStyle(u.status.contains(.admin) ? Color(hex: 0xE10000) : Color.accentColor) - .opacity(u.status.contains(.idle) ? 0.5 : 1.0) - } - .scrollBounceBehavior(.basedOnSize) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(model.serverTitle) - .font(.headline) - } - ToolbarItem(placement: .navigationBarLeading) { - Button { - model.disconnect() - } label: { - Text(Image(systemName: "xmark.circle.fill")) - .symbolRenderingMode(.hierarchical) - .font(.title2) - .foregroundColor(.secondary) - } - } - } - } - } -} - -#Preview { - ChatView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) -} |