aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Views
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-19 20:38:13 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-19 20:38:13 -0800
commit4fd69c02a3e7b581bb9229865336c315153f3b18 (patch)
treee6e11d872424196f2b4033077902126ad5556e40 /Hotline/Views
parent5e87b5927cd931d46fb5f72fb035480a95969a9f (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.swift153
-rw-r--r--Hotline/Views/FilesView.swift140
-rw-r--r--Hotline/Views/MessageBoardView.swift74
-rw-r--r--Hotline/Views/NewsView.swift205
-rw-r--r--Hotline/Views/ServerView.swift51
-rw-r--r--Hotline/Views/TrackerView.swift397
-rw-r--r--Hotline/Views/UsersView.swift41
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()))
-}