aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-06 22:52:09 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-06 22:52:09 -0800
commite3392bb948ad53889ad334140952f2acf614668e (patch)
tree74937a9202b3bb71f28ef9d8e0b3560bdd195b49 /Hotline
parent195af38221d1a4358455ba8501b87e4098da101b (diff)
Some work on news. More UI tweaks across the board.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.pngbin8399 -> 8190 bytes
-rw-r--r--Hotline/Assets.xcassets/Hotline.imageset/Contents.json55
-rw-r--r--Hotline/Assets.xcassets/Hotline.imageset/hotline-black.svg3
-rw-r--r--Hotline/Assets.xcassets/Hotline.imageset/hotline-white.svg3
-rw-r--r--Hotline/Assets.xcassets/Image.imageset/Contents.json20
-rw-r--r--Hotline/Hotline/HotlineClient.swift26
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift49
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift48
-rw-r--r--Hotline/Utility/FoundationExtensions.swift (renamed from Hotline/Utility/DataExtensions.swift)10
-rw-r--r--Hotline/Utility/ObservableScrollView.swift38
-rw-r--r--Hotline/Utility/SwiftUIExtensions.swift7
-rw-r--r--Hotline/Views/ChatView.swift6
-rw-r--r--Hotline/Views/FilesView.swift2
-rw-r--r--Hotline/Views/NewsView.swift89
-rw-r--r--Hotline/Views/TrackerView.swift148
-rw-r--r--Hotline/Views/UserListView.swift16
16 files changed, 413 insertions, 107 deletions
diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png b/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png
index 02d1c69..c81665b 100644
--- a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png
+++ b/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Hotline.imageset/Contents.json b/Hotline/Assets.xcassets/Hotline.imageset/Contents.json
new file mode 100644
index 0000000..ebc37a0
--- /dev/null
+++ b/Hotline/Assets.xcassets/Hotline.imageset/Contents.json
@@ -0,0 +1,55 @@
+{
+ "images" : [
+ {
+ "filename" : "hotline-black.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "filename" : "hotline-white.svg",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Hotline/Assets.xcassets/Hotline.imageset/hotline-black.svg b/Hotline/Assets.xcassets/Hotline.imageset/hotline-black.svg
new file mode 100644
index 0000000..5c98c36
--- /dev/null
+++ b/Hotline/Assets.xcassets/Hotline.imageset/hotline-black.svg
@@ -0,0 +1,3 @@
+<svg width="80" height="160" viewBox="0 0 80 160" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 23L30 0V64H50V23L80 23L80 136L50 160V95.1256L30 95.0595V137H0V23Z" fill="black"/>
+</svg>
diff --git a/Hotline/Assets.xcassets/Hotline.imageset/hotline-white.svg b/Hotline/Assets.xcassets/Hotline.imageset/hotline-white.svg
new file mode 100644
index 0000000..967ea62
--- /dev/null
+++ b/Hotline/Assets.xcassets/Hotline.imageset/hotline-white.svg
@@ -0,0 +1,3 @@
+<svg width="80" height="160" viewBox="0 0 80 160" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 23L30 0V64H50V23L80 23L80 136L50 160V95.1256L30 95.0595V137H0V23Z" fill="white"/>
+</svg>
diff --git a/Hotline/Assets.xcassets/Image.imageset/Contents.json b/Hotline/Assets.xcassets/Image.imageset/Contents.json
new file mode 100644
index 0000000..a19a549
--- /dev/null
+++ b/Hotline/Assets.xcassets/Image.imageset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index f108c79..9d62811 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -51,12 +51,12 @@ class HotlineClient {
])
var connectionStatus: HotlineClientStatus = .disconnected
- var agreement: String?
var users: [UInt16:HotlineUser] = [:]
var userList: [HotlineUser] = []
var chatMessages: [HotlineChat] = []
var messageBoardMessages: [String] = []
var fileList: [HotlineFile] = []
+ var newsCategories: [HotlineNewsCategory] = []
var userName: String = "bolt"
var userIconID: UInt16 = 128
@@ -118,13 +118,14 @@ class HotlineClient {
}
func reset() {
+ self.transactionLog = [:]
DispatchQueue.main.async {
self.chatMessages = []
- self.agreement = nil
self.users = [:]
self.userList = []
self.messageBoardMessages = []
self.fileList = []
+ self.newsCategories = []
}
}
@@ -370,8 +371,11 @@ class HotlineClient {
self.sendTransaction(t, callback: callback)
}
- func sendGetNewsArticles(callback: (() -> Void)? = nil) {
- let t = HotlineTransaction(type: .getNewsArticleNameList)
+ func sendGetNewsArticles(path: [String]? = nil, callback: (() -> Void)? = nil) {
+ var t = HotlineTransaction(type: .getNewsArticleNameList)
+ if path != nil {
+ t.setFieldPath(type: .newsPath, val: path!)
+ }
self.sendTransaction(t, callback: callback)
}
@@ -383,6 +387,11 @@ class HotlineClient {
self.sendTransaction(t, callback: callback)
}
+// func sendGetNews(callback: (() -> Void)? = nil) {
+// let t = HotlineTransaction(type: .getNewsFile)
+// self.sendTransaction(t, callback: callback)
+// }
+
// MARK: - Incoming
private func processReply(_ transaction: HotlineTransaction) {
@@ -460,10 +469,15 @@ class HotlineClient {
self.fileList = files
}
case .getNewsCategoryNameList:
+ var categories: [HotlineNewsCategory] = []
for fi in transaction.getFieldList(type: .newsCategoryListData15) {
let c = fi.getNewsCategory()
+ categories.append(c)
print("CATEGORY: \(c)")
}
+ DispatchQueue.main.async {
+ self.newsCategories = categories
+ }
default:
break
}
@@ -508,7 +522,7 @@ class HotlineClient {
print("HotlineClient ❌")
self.disconnect()
case .showAgreement:
- if let noAgreementField = transaction.getField(type: .noServerAgreement) {
+ if let _ = transaction.getField(type: .noServerAgreement) {
print("NO AGREEMENT?")
}
if let agreementParam = transaction.getField(type: .data) {
@@ -518,7 +532,7 @@ class HotlineClient {
print("\n--------------------------\n\n")
DispatchQueue.main.async {
self.chatMessages.insert(HotlineChat(text: agreementText, type: .agreement), at: 0)
- self.agreement = agreementText
+// self.agreement = agreementText
}
// self.sendAgree() {
// self.sendGetUserList()
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index 0cf3d06..a0e129e 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -55,16 +55,18 @@ struct HotlineNewsCategory: Identifiable, Hashable {
if self.type == 2 {
// Read bundle properties
self.count = data.readUInt16(at: 2)!
-
- let nameSize = data.readUInt8(at: 4)!
- self.name = data.readString(at: 5, length: Int(nameSize))!
+ let (n, _) = data.readPString(at: 4)
+ self.name = n!
}
else if self.type == 3 {
// Read category properties
self.count = data.readUInt16(at: 2)!
-
- let nameSize = data.readUInt8(at: 2 + 2 + 4 + 4)!
- self.name = data.readString(at: 2 + 2 + 4 + 4 + 1, length: Int(nameSize))!
+// let guid = data.readUInt32(at: 4)!
+// print("CATEGORY GUID: \(guid)")
+// let addSN = data.readUInt32(at: 20)!
+// let removeSN = data.readUInt32(at: 24)!
+ let (n, _) = data.readPString(at: 28)
+ self.name = n!
}
else {
self.count = 0
@@ -221,6 +223,37 @@ struct HotlineTransactionField {
init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) {
self.init(type: type, string: string, encoding: .ascii, encrypt: encrypt)
}
+
+ init(type: HotlineTransactionFieldType, path: String) {
+ var components: [String] = []
+
+ for component in path.components(separatedBy: "/") {
+ if !component.isEmpty {
+ components.append(component)
+ }
+ }
+
+ self.init(type: type, pathComponents: components)
+ }
+
+ init(type: HotlineTransactionFieldType, pathComponents: [String]) {
+ var pathData = Data()
+
+ pathData.appendUInt16(UInt16(pathComponents.count))
+ for name in pathComponents {
+ pathData.appendUInt16(0)
+
+ var nameData = name.data(using: .ascii, allowLossyConversion: true)
+ if nameData == nil {
+ nameData = Data()
+ }
+
+ pathData.appendUInt8(UInt8(nameData!.count))
+ pathData.append(nameData!)
+ }
+
+ self.init(type: type, dataSize: UInt16(pathData.count), data: pathData)
+ }
func getUInt8() -> UInt8? {
return self.data.readUInt8(at: 0)
@@ -325,6 +358,10 @@ struct HotlineTransaction {
self.fields.append(HotlineTransactionField(type: type, string: val))
}
+ mutating func setFieldPath(type: HotlineTransactionFieldType, val: [String]) {
+ self.fields.append(HotlineTransactionField(type: type, pathComponents: val))
+ }
+
func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? {
return self.fields.first { p in
p.type == type
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift
index 4ae252c..78f681f 100644
--- a/Hotline/Hotline/HotlineTrackerClient.swift
+++ b/Hotline/Hotline/HotlineTrackerClient.swift
@@ -246,24 +246,40 @@ class HotlineTrackerClient {
let ip_3 = self.bytes.readUInt8(at: cursor + 2),
let ip_4 = self.bytes.readUInt8(at: cursor + 3),
let port = self.bytes.readUInt16(at: cursor + 4),
- let userCount = self.bytes.readUInt16(at: cursor + 6),
- let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) {
+ let userCount = self.bytes.readUInt16(at: cursor + 6) {
+// let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) {
- let nameLength = Int(nameLengthByte)
- if let name = self.bytes.readString(at: cursor + 11, length: nameLength) {
- if let descLengthByte = self.bytes.readUInt8(at: cursor + 11 + nameLength) {
- let descLength = Int(descLengthByte)
- if let desc = self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength) {
- let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
-
- print("SERVER: \(server)")
-
- foundServers.append(server)
-
- cursor += 11 + nameLength + 1 + descLength
- }
- }
+ let (serverName, nameByteCount) = self.bytes.readPString(at: cursor + 10)
+ let (serverDescription, descByteCount) = self.bytes.readPString(at: cursor + 10 + nameByteCount)
+
+ if let name = serverName,
+ let desc = serverDescription {
+ let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
+
+ print("SERVER: \(server)")
+
+ foundServers.append(server)
+
+
}
+
+ cursor += 10 + nameByteCount + descByteCount
+
+// let nameLength = Int(nameLengthByte)
+// if let name = self.bytes.readString(at: cursor + 11, length: nameLength) {
+// if let descLengthByte = self.bytes.readUInt8(at: cursor + 11 + nameLength) {
+// let descLength = Int(descLengthByte)
+// if let desc = self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength) {
+// let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
+//
+// print("SERVER: \(server)")
+//
+// foundServers.append(server)
+//
+// cursor += 11 + nameLength + 1 + descLength
+// }
+// }
+// }
}
print(cursor)
diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index aefd204..c6ca251 100644
--- a/Hotline/Utility/DataExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -62,6 +62,16 @@ extension Data {
return str
}
+ func readPString(at offset: Int) -> (String?, Int) {
+ let len = Int(self.readUInt8(at: offset)!)
+ return (self.readString(at: offset+1, length: len), 1 + len)
+ }
+
+ func readLongPString(at offset: Int) -> (String?, Int) {
+ let len = Int(self.readUInt16(at: offset)!)
+ return (self.readString(at: offset+2, length: len), 2 + len)
+ }
+
mutating func appendUInt8(_ value: UInt8, endianness: Endianness = .big) {
var val = endianness == .big ? value.bigEndian : value.littleEndian
diff --git a/Hotline/Utility/ObservableScrollView.swift b/Hotline/Utility/ObservableScrollView.swift
new file mode 100644
index 0000000..a6f46cc
--- /dev/null
+++ b/Hotline/Utility/ObservableScrollView.swift
@@ -0,0 +1,38 @@
+import SwiftUI
+
+struct ScrollViewOffsetPreferenceKey: PreferenceKey {
+ typealias Value = CGFloat
+ static var defaultValue = CGFloat.zero
+ static func reduce(value: inout Value, nextValue: () -> Value) {
+ value += nextValue()
+ }
+}
+
+struct ObservableScrollView<Content>: View where Content : View {
+ @Namespace var scrollSpace
+ @Binding var scrollOffset: CGFloat
+ let content: () -> Content
+
+ init(scrollOffset: Binding<CGFloat>,
+ @ViewBuilder content: @escaping () -> Content) {
+ _scrollOffset = scrollOffset
+ self.content = content
+ }
+
+ var body: some View {
+ ScrollView {
+ content()
+ .background(GeometryReader { geo in
+ let offset = -geo.frame(in: .named(scrollSpace)).minY
+ Color.clear
+ .preference(key: ScrollViewOffsetPreferenceKey.self,
+ value: offset)
+ })
+
+ }
+ .coordinateSpace(name: scrollSpace)
+ .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
+ scrollOffset = value
+ }
+ }
+}
diff --git a/Hotline/Utility/SwiftUIExtensions.swift b/Hotline/Utility/SwiftUIExtensions.swift
new file mode 100644
index 0000000..3b850e0
--- /dev/null
+++ b/Hotline/Utility/SwiftUIExtensions.swift
@@ -0,0 +1,7 @@
+import SwiftUI
+
+extension Color {
+ init(hex: Int, opacity: Double = 1.0) {
+ self.init(red: Double((hex >> 16) & 0xFF) / 255.0, green: Double((hex >> 8) & 0xFF) / 255.0, blue: Double(hex & 0xFF) / 255.0, opacity: opacity)
+ }
+}
diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift
index dc6e1d4..1aee6b2 100644
--- a/Hotline/Views/ChatView.swift
+++ b/Hotline/Views/ChatView.swift
@@ -2,6 +2,7 @@ import SwiftUI
struct ChatView: View {
@Environment(HotlineClient.self) private var hotline
+ @Environment(\.colorScheme) var colorScheme
@State var input: String = ""
@State private var scrollPos: Int?
@@ -17,13 +18,12 @@ struct ChatView: View {
LazyVStack(alignment: .leading) {
ForEach(hotline.chatMessages) { msg in
if msg.type == .agreement {
-
VStack(alignment: .leading) {
Text(msg.text)
.padding()
.opacity(0.75)
- .background(Color(white: 0.96))
- .cornerRadius(20)
+ .background(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96))
+ .cornerRadius(16)
}
.padding()
}
diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift
index 1b35695..3b2b236 100644
--- a/Hotline/Views/FilesView.swift
+++ b/Hotline/Views/FilesView.swift
@@ -57,6 +57,8 @@ struct FilesView: View {
else {
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
Image(uiImage: fileIcon(name: tree.name))
+ .renderingMode(.template)
+ .foregroundColor(.accentColor)
}
.frame(minWidth: 25)
Text(tree.name).bold()
diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift
index 5f2dbe9..9c4d34a 100644
--- a/Hotline/Views/NewsView.swift
+++ b/Hotline/Views/NewsView.swift
@@ -5,32 +5,89 @@ struct NewsView: View {
@Environment(HotlineClient.self) private var hotline
@State private var fetched = false
+ @State private var selectedCategory: HotlineNewsCategory? = nil
+ @State private var topListHeight: CGFloat = 200
+ @State private var dividerHeight: CGFloat = 30
+
+ var topList: some View {
+
+ // Your list content goes here
+ List {
+ ForEach(hotline.newsCategories, id: \.self) { cat in
+ DisclosureGroup {
+ ProgressView(value: 0.4)
+ .task {
+ print("EXPANDED?", cat.name)
+ hotline.sendGetNewsArticles(path: [cat.name]) {
+ print("OK")
+ }
+ }
+ } label: {
+ Text(cat.name)
+ .fontWeight(.medium)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ }
+ }
+// .listStyle(.plain)
+ }
+
+ var bottomList: some View {
+ // Your list content goes here
+ ScrollView(.vertical) {
+ HStack(alignment: .top, spacing: 0) {
+ Text("HELLO")
+ .multilineTextAlignment(.leading)
+ Spacer()
+ }
+ .padding()
+ }
+ }
var body: some View {
-// @Bindable var config = appState
NavigationStack {
- ScrollView {
- LazyVStack(alignment: .leading) {
-// ForEach(hotline.messageBoardMessages, id: \.self) {
-// Text($0)
-// .lineLimit(100)
-// .padding()
-// .textSelection(.enabled)
-// Divider()
-// }
+ VStack(spacing: 0) {
+ topList
+ .frame(height: topListHeight)
+ VStack(alignment: .center) {
+ Divider()
+ Spacer()
+ HStack(alignment: .center) {
+ Image(systemName: "line.3.horizontal")
+ .opacity(0.2)
+ .font(.system(size: 14))
+ }
+ Spacer()
+// Divider()
}
- Spacer()
+ .background(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)
+ // bottomListHeight = max(min(bottomListHeight - delta, 400), 0)
+ }
+ )
+ bottomList
}
.task {
if !fetched {
hotline.sendGetNewsCategories() {
fetched = true
}
+
+ // hotline.sendGetNewsArticles(path: ["News"]) {
+ // print("GOT ARTICLES?")
+ // }
}
}
- .refreshable {
- hotline.sendGetNewsCategories()
- }
+ // .refreshable {
+ // hotline.sendGetNewsCategories()
+ // }
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
@@ -52,8 +109,8 @@ struct NewsView: View {
} label: {
Image(systemName: "square.and.pencil")
-// .symbolRenderingMode(.hierarchical)
-// .foregroundColor(.secondary)
+ // .symbolRenderingMode(.hierarchical)
+ // .foregroundColor(.secondary)
}
}
diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift
index 3ed66dd..7f59118 100644
--- a/Hotline/Views/TrackerView.swift
+++ b/Hotline/Views/TrackerView.swift
@@ -11,6 +11,7 @@ struct TrackerView: View {
@Environment(\.colorScheme) var colorScheme
@State private var selectedServer: HotlineServer?
+ @State var scrollOffset: CGFloat = CGFloat.zero
func shouldDisplayDescription(server: HotlineServer) -> Bool {
guard let name = server.name, let desc = server.description else {
@@ -36,83 +37,126 @@ struct TrackerView: View {
}
}
+ func inverseLerp(lower: Double, upper: Double, v: Double) -> Double {
+ return (v - lower) / (upper - lower)
+ }
+
var body: some View {
@Bindable var config = appState
@Bindable var client = hotline
- ScrollView {
- LazyVStack(alignment: .leading) {
- ForEach(tracker.servers) { server in
- VStack(alignment: .leading) {
- HStack(alignment: .firstTextBaseline) {
- Image(systemName: "globe.americas.fill").font(.title3)
- VStack(alignment: .leading) {
- Text(server.name!).font(.title3).fontWeight(.medium)
- if shouldDisplayDescription(server: server) {
+ ZStack(alignment: .center) {
+ VStack(alignment: .center) {
+ ZStack(alignment: .top) {
+ Image("Hotline")
+ .resizable()
+ .renderingMode(.template)
+ .foregroundColor(Color(hex: 0xE10000))
+ .scaledToFit()
+ .frame(width: 40.0, height: 40.0)
+ HStack(alignment: .center) {
+ Spacer()
+ Button {
+ // hotline.disconnect()
+ } label: {
+ Text(Image(systemName: "point.3.connected.trianglepath.dotted"))
+ .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.primary)
+ .font(.title2)
+ .padding(.trailing, 16)
+ }
+ }
+ .frame(height: 40.0)
+ }
+ .padding()
+ .opacity(scrollOffset > 80 ? 0 : 1.0)
+// .padding(.top, 5)
+ .opacity(inverseLerp(lower: -80, upper: 0, v: scrollOffset))
+// .opacity(inverseLerp(lower: 20, upper: 0, v: scrollOffset))
+
+ Spacer()
+ }
+ ObservableScrollView(scrollOffset: $scrollOffset) {
+ LazyVStack(alignment: .leading) {
+ ForEach(tracker.servers) { server in
+ VStack(alignment: .leading) {
+ HStack(alignment: .firstTextBaseline) {
+ Image(systemName: "globe.americas.fill").font(.title3)
+ VStack(alignment: .leading) {
+ Text(server.name!).font(.title3).fontWeight(.medium)
+ if shouldDisplayDescription(server: server) {
+ Spacer()
+ Text(server.description!).opacity(0.5).font(.system(size: 16))
+ }
Spacer()
- Text(server.description!).opacity(0.4).font(.system(size: 16))
+ Text("\(server.address)").opacity(0.3).font(.system(size: 13))
}
Spacer()
- Text("\(server.address)").opacity(0.2).font(.system(size: 13))
- }
- Spacer()
- Text("\(server.users)").opacity(0.2).font(.system(size: 16)).fontWeight(.medium)
- }
- if server == selectedServer {
- Spacer(minLength: 16)
-
- if hotline.server == server && hotline.connectionStatus != .disconnected {
- ProgressView("", value: connectionStatusToProgress(status: hotline.connectionStatus))
+ Text("\(server.users)").opacity(0.3).font(.system(size: 16)).fontWeight(.medium)
}
- else {
- Button("Connect") {
- hotline.connect(to: server)
+ if server == selectedServer {
+ Spacer(minLength: 16)
+
+ if hotline.server == server && hotline.connectionStatus != .disconnected {
+ ProgressView(value: connectionStatusToProgress(status: hotline.connectionStatus))
+ .frame(minHeight: 10)
+ .accentColor(colorScheme == .dark ? .white : .black)
+ }
+ else {
+ Button("Connect") {
+ hotline.connect(to: server)
+ }
+ .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)
}
- .bold()
- .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
- .frame(maxWidth: .infinity)
- .foregroundColor(.black)
- .background(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(0.4)
- )
- .cornerRadius(10.0)
}
}
- }
- .multilineTextAlignment(.leading)
- .padding()
- .background(colorScheme == .dark ? Color(white: 0.1) : .white)
- .cornerRadius(20)
- .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
+ .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))
+ }
+ .refreshable {
+ await withCheckedContinuation { continuation in
+ tracker.fetch() {
+ continuation.resume()
+ }
}
- .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
}
+
}
.fullScreenCover(isPresented: Binding(get: { return hotline.connectionStatus == .loggedIn }, set: { _ in }), onDismiss: {
hotline.disconnect()
}) {
ServerView()
}
- .background(colorScheme == .dark ? .black : Color(white: 0.95))
+ .background(Color(uiColor: UIColor.systemGroupedBackground))
.frame(maxWidth: .infinity)
.task {
tracker.fetch()
}
- .refreshable {
- await withCheckedContinuation { continuation in
- tracker.fetch() {
- continuation.resume()
- }
- }
- }
- .navigationTitle("Tracker")
- .navigationBarTitleDisplayMode(.inline)
}
}
diff --git a/Hotline/Views/UserListView.swift b/Hotline/Views/UserListView.swift
index cae1d0c..e0fa7fb 100644
--- a/Hotline/Views/UserListView.swift
+++ b/Hotline/Views/UserListView.swift
@@ -5,15 +5,15 @@ struct UserListView: View {
var body: some View {
NavigationStack {
- VStack(spacing: 0) {
- List(hotline.userList) { u in
- HStack(alignment: .firstTextBaseline) {
- Text(u.name).bold().foregroundStyle(u.isAdmin ? Color.red : Color.black).opacity(u.isIdle ? 0.5 : 1.0)
- }
- }
- .listStyle(.plain)
- .padding()
+ List(hotline.userList) { u in
+ Text(u.name)
+ .fontWeight(.medium)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .foregroundStyle(u.isAdmin ? Color(hex: 0xE10000) : Color.accentColor)
+ .opacity(u.isIdle ? 0.5 : 1.0)
}
+// .listStyle(.grouped)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {