diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-05 15:57:27 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-05 15:57:27 -0800 |
| commit | b99219f16792be4a06a89f1f32a59f0bf97bba68 (patch) | |
| tree | 4503d6f8b8c980c073c0e899da014bbffc4f7e94 | |
| parent | b1a176ba3da2c7cf815084f0f8109008fe763809 (diff) | |
Experimenting with files.
| -rw-r--r-- | Hotline.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 11 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 63 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 8 | ||||
| -rw-r--r-- | Hotline/Views/FileListView.swift | 22 | ||||
| -rw-r--r-- | Hotline/Views/FilesView.swift | 11 | ||||
| -rw-r--r-- | Hotline/Views/HotlineView.swift | 7 | ||||
| -rw-r--r-- | Hotline/Views/ServerView.swift | 155 | ||||
| -rw-r--r-- | Hotline/Views/TrackerServerView.swift | 100 | ||||
| -rw-r--r-- | Hotline/Views/TrackerView.swift | 86 |
10 files changed, 336 insertions, 131 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index 93931f6..f6aa976 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ DA0D698D2B1E7CF700C71DF5 /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D698C2B1E7CF700C71DF5 /* UserListView.swift */; }; DA0D698F2B1E841600C71DF5 /* MessageBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D698E2B1E841600C71DF5 /* MessageBoardView.swift */; }; DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D69902B1E894800C71DF5 /* FilesView.swift */; }; + DA161B6C2B1FA520000918F3 /* FileListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA161B6B2B1FA520000918F3 /* FileListView.swift */; }; DA43205C2B1D003B00FC8843 /* HotlineClientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA43205B2B1D003B00FC8843 /* HotlineClientModel.swift */; }; DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA43205D2B1D615600FC8843 /* ServerView.swift */; }; DA4F2BF62B1668A700D8ADDC /* HotlineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F2BF52B1668A700D8ADDC /* HotlineClient.swift */; }; @@ -32,6 +33,7 @@ DA0D698C2B1E7CF700C71DF5 /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = "<group>"; }; DA0D698E2B1E841600C71DF5 /* MessageBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBoardView.swift; sourceTree = "<group>"; }; DA0D69902B1E894800C71DF5 /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = "<group>"; }; + DA161B6B2B1FA520000918F3 /* FileListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListView.swift; sourceTree = "<group>"; }; DA43205B2B1D003B00FC8843 /* HotlineClientModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClientModel.swift; sourceTree = "<group>"; }; DA43205D2B1D615600FC8843 /* ServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerView.swift; sourceTree = "<group>"; }; DA4F2BF52B1668A700D8ADDC /* HotlineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClient.swift; sourceTree = "<group>"; }; @@ -71,6 +73,7 @@ DA4F2C002B1A558E00D8ADDC /* ChatView.swift */, DA0D698C2B1E7CF700C71DF5 /* UserListView.swift */, DA0D69902B1E894800C71DF5 /* FilesView.swift */, + DA161B6B2B1FA520000918F3 /* FileListView.swift */, DA4F2BFC2B1A52EF00D8ADDC /* TrackerServerView.swift */, DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */, ); @@ -203,6 +206,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DA161B6C2B1FA520000918F3 /* FileListView.swift in Sources */, DA43205C2B1D003B00FC8843 /* HotlineClientModel.swift in Sources */, DA4F2BFB2B1A518500D8ADDC /* AgreementView.swift in Sources */, DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */, diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index de01ae9..d2e3954 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -48,6 +48,7 @@ class HotlineClient { var userList: [HotlineUser] = [] var chatMessages: [HotlineChat] = [] var messageBoard: String = "" + var fileList: [HotlineFile] = [] var userName: String = "bolt" var userIconID: UInt16 = 128 @@ -406,6 +407,16 @@ class HotlineClient { self.messageBoard = text } } + case .getFileNameList: + var files: [HotlineFile] = [] + for fi in transaction.getFieldList(type: .fileNameWithInfo) { + let file = fi.getFile() +// print("GOT FILE: \(file.name) \(file.creator) \(file.type) \(file.fileSize)") + files.append(file) + } + DispatchQueue.main.async { + self.fileList = files + } default: break } diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index 20168b0..3eb4a7d 100644 --- a/Hotline/Hotline/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -17,6 +17,65 @@ struct HotlineServer: Identifiable, Hashable { } } +extension UInt32 { + func decodeFromUInt32() -> String { + let bytes = [ + UInt8((self >> 24) & 0xFF), + UInt8((self >> 16) & 0xFF), + UInt8((self >> 8) & 0xFF), + UInt8(self & 0xFF) + ] + return String(bytes: bytes, encoding: .utf8) ?? "" + } +} + +struct HotlineFile: Identifiable, Hashable { + let id = UUID() + let type: String + let creator: String + let fileSize: UInt32 + let name: String + + var isExpanded: Bool = false + var files: [HotlineFile] = [] + + let isFolder: Bool + + static func == (lhs: HotlineFile, rhs: HotlineFile) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + init(type: String, creator: String, fileSize: UInt32, fileName: String) { + self.type = type + self.creator = creator + self.fileSize = fileSize + self.name = fileName + + self.isFolder = (self.type == "fldr") + } + + init(from data: Data) { + let typeRaw = data.readUInt32(at: 0)! + let creatorRaw = data.readUInt32(at: 4)! + + self.type = typeRaw.decodeFromUInt32() + self.creator = creatorRaw.decodeFromUInt32() + self.fileSize = data.readUInt32(at: 8)! + + self.isFolder = (self.type == "fldr") + +// data.readUInt32(at: 12)! // reserved +// let nameScript = data.readUInt16(at: 16)! // name script + + let nameLength = data.readUInt16(at: 18)! + self.name = data.readString(at: 20, length: Int(nameLength), encoding: .ascii)! + } +} + struct HotlineUser: Identifiable, Hashable { let id: UInt16 let iconID: UInt16 @@ -158,6 +217,10 @@ struct HotlineTransactionField { func getUser() -> HotlineUser { return HotlineUser(from: self.data) } + + func getFile() -> HotlineFile { + return HotlineFile(from: self.data) + } } struct HotlineTransaction { diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index fd4e97b..cff3888 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -45,9 +45,9 @@ class HotlineTrackerClient { self.serverPort = NWEndpoint.Port(rawValue: tracker.port)! } - func fetch() { + func fetch(_ callback: (() -> Void)? = nil) { self.reset() - self.connect() + self.connect(callback) } private func reset() { @@ -55,7 +55,7 @@ class HotlineTrackerClient { self.serverCount = 0 } - private func connect() { + private func connect(_ callback: (() -> Void)? = nil) { self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp) self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in switch newState { @@ -69,11 +69,13 @@ class HotlineTrackerClient { print("CONNECTION CANCELLED") DispatchQueue.main.async { self?.connectionStatus = .disconnected + callback?() } case .failed(let err): print("CONNECTION ERROR \(err)") DispatchQueue.main.async { self?.connectionStatus = .disconnected + callback?() } default: print("CONNECTION OTHER THING") diff --git a/Hotline/Views/FileListView.swift b/Hotline/Views/FileListView.swift new file mode 100644 index 0000000..852fce4 --- /dev/null +++ b/Hotline/Views/FileListView.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct FileListView: View { + var item: HotlineFile + + var body: some View { + List { + ForEach(item.files) { f in + if f.isFolder { + DisclosureGroup(f.name, isExpanded: false) + } + else { + Text("HELLO") + } + } + } + } +} + +#Preview { + FileListView(item: HotlineFile(type: "fldr", creator: "", fileSize: 0, fileName: "Folder")) +} diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift index 0e1bb91..d910af2 100644 --- a/Hotline/Views/FilesView.swift +++ b/Hotline/Views/FilesView.swift @@ -6,15 +6,8 @@ struct FilesView: View { @State private var fetched = false var body: some View { - ScrollView { - VStack(spacing: 0) { - List(hotline.userList) { u in - HStack(alignment: .firstTextBaseline) { - Text(u.name).bold().foregroundStyle(u.isAdmin ? Color.red : Color.black) - } - } - .padding() - } + List(hotline.fileList) { + FileListView(item: $0) } .task { if !fetched { diff --git a/Hotline/Views/HotlineView.swift b/Hotline/Views/HotlineView.swift index fe67f79..713691f 100644 --- a/Hotline/Views/HotlineView.swift +++ b/Hotline/Views/HotlineView.swift @@ -5,20 +5,15 @@ struct HotlineView: View { @Environment(HotlineClient.self) private var hotline @Environment(HotlineTrackerClient.self) private var tracker - @State private var isTrackerVisible = true - var body: some View { @Bindable var config = appState NavigationStack { - ServerView() + TrackerView() } .sheet(isPresented: $config.agreementPresented) { AgreementView(text: hotline.agreement!) } - .sheet(isPresented: $config.trackerPresented) { - TrackerView() - } } } diff --git a/Hotline/Views/ServerView.swift b/Hotline/Views/ServerView.swift index 0dafbce..98fd515 100644 --- a/Hotline/Views/ServerView.swift +++ b/Hotline/Views/ServerView.swift @@ -4,70 +4,123 @@ struct ServerView: View { @Environment(HotlineState.self) private var appState @Environment(HotlineClient.self) private var hotline @Environment(HotlineTrackerClient.self) private var tracker - + + let server: HotlineServer + + func connectionStatusTitle(status: HotlineClientStatus) -> String { + switch(status) { + case .disconnected: + return "Disconnected" + case .connecting: + return "Connecting" + case .connected: + return "Connected" + case .loggingIn: + return "Logging In" + case .loggedIn: + return "Logged In" + } + } + + func connectionProgress(status: HotlineClientStatus) -> Double { + return Double(status.rawValue) / Double(HotlineClientStatus.loggedIn.rawValue) + } + var body: some View { @Bindable var config = appState - TabView { - NavigationView { - ChatView() - .navigationTitle(hotline.server?.name ?? "Hotline") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems( - leading: Button(action: { - appState.presentTracker() - }) { - Image(systemName: "globe.americas.fill") // Hamburger icon or similar - .imageScale(.large) + NavigationStack { + if hotline.connectionStatus != .loggedIn { + VStack { + Spacer() + VStack(alignment: .center) { + Text("🌎").font(.largeTitle) + Text(server.name!).font(.title3).fontWeight(.medium) + Text(server.description!).opacity(0.6).font(.title3) + Text(server.address).opacity(0.6).font(.title3) + } + Spacer() + HStack(alignment: .center) { + if hotline.connectionStatus == .disconnected { + ProgressView(connectionStatusTitle(status: hotline.connectionStatus), value: connectionProgress(status: hotline.connectionStatus)) + Button("Connect") { + hotline.connect(to: server) + // config.dismissTracker() + } + .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) } - ) -// .toolbarBackground(.visible, for: .navigationBar) -// .toolbarBackground(.red, for: .navigationBar) - } - .tabItem { - Image(systemName: "message") - } - - NavigationView { - UserListView() - .navigationTitle("User List") - .navigationBarTitleDisplayMode(.inline) - } - .tabItem { - Image(systemName: "person.fill") - } - - Text("News") - .tabItem { - Image(systemName: "newspaper") + else { + ProgressView("", value: Double(hotline.connectionStatus.rawValue / HotlineClientStatus.loggedIn.rawValue)) + } + } } - - NavigationView { - MessageBoardView() - .navigationTitle("Message Board") - .navigationBarTitleDisplayMode(.inline) + .padding() } - .tabItem { - Image(systemName: "pin") + else { + TabView { + ChatView() + .navigationTitle(hotline.server?.name ?? "Hotline") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems( + leading: Button(action: { + appState.presentTracker() + }) { + Image(systemName: "globe.americas.fill") // Hamburger icon or similar + .imageScale(.large) + } + ) + // .toolbarBackground(.visible, for: .navigationBar) + // .toolbarBackground(.red, for: .navigationBar) + .tabItem { + Image(systemName: "message") + } + + UserListView() + .navigationTitle("User List") + .navigationBarTitleDisplayMode(.inline) + .tabItem { + Image(systemName: "person.fill") + } + + Text("News") + .tabItem { + Image(systemName: "newspaper") + } + + MessageBoardView() + .navigationTitle("Message Board") + .navigationBarTitleDisplayMode(.inline) + .tabItem { + Image(systemName: "pin") + } + + FilesView() + .navigationTitle("Files") + .navigationBarTitleDisplayMode(.inline) + .tabItem { + Image(systemName: "folder") + } + } } - NavigationView { - FilesView() - .navigationTitle("Files") - .navigationBarTitleDisplayMode(.inline) - } - .tabItem { - Image(systemName: "folder") - } + // .sheet(isPresented: Binding(get: { hotline.connectionStatus != .loggedIn }, set: { _ in })) { + // TrackerView() + // } } -// .sheet(isPresented: Binding(get: { hotline.connectionStatus != .loggedIn }, set: { _ in })) { -// TrackerView() -// } } } #Preview { - ServerView() + ServerView(server: HotlineServer(address: "192.168.1.1", port: 5050, users: 5, name: "Ye Olde Server", description: "This is a server")) .environment(HotlineClient()) .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) + .environment(HotlineState()) } diff --git a/Hotline/Views/TrackerServerView.swift b/Hotline/Views/TrackerServerView.swift index 2c0f3e3..5288496 100644 --- a/Hotline/Views/TrackerServerView.swift +++ b/Hotline/Views/TrackerServerView.swift @@ -3,50 +3,92 @@ import SwiftUI struct TrackerServerView: View { @Environment(HotlineState.self) private var appState @Environment(HotlineClient.self) private var hotline - @Environment(\.dismiss) private var dismiss let server: HotlineServer + @State private var expanded = false + + func shouldDisplayDescription(server: HotlineServer) -> Bool { + guard let name = server.name, let desc = server.description else { + return false + } + + return desc.count > 0 && desc != name && !desc.contains(/^-+/) + } + var body: some View { @Bindable var config = appState VStack(alignment: .leading) { - HStack { - Text("🌎").dynamicTypeSize(.xxxLarge) - Text(server.name!).bold().dynamicTypeSize(.xxxLarge) - } - .padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) - - Text(server.description!).opacity(0.6).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)).textSelection(.enabled) - Text(server.address).opacity(0.3).dynamicTypeSize(.medium).textSelection(.enabled) - - Spacer() - - HStack(alignment: .center) { - Button("Connect") { - hotline.connect(to: server) - config.dismissTracker() -// dismiss() + HStack(alignment: .firstTextBaseline) { + Text("🌎").font(.title3) + VStack(alignment: .leading) { + Text(server.name!).font(.title3).fontWeight(.medium) + if shouldDisplayDescription(server: server) { + Text(server.description!).opacity(0.6).font(.title3) + } } - .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) } + .padding() + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden) + .listRowBackground(Color(white: 0.96)) + + // .padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) +// ProgressView(value: 0.5) +// +// HStack(alignment: .center) { +// Button("Connect") { +// hotline.connect(to: server) +// config.dismissTracker() +// // dismiss() +// } +// .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) +// } } - .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0)) +// label: { +// HStack(alignment: .firstTextBaseline) { +// Text("🌎").font(.title3) +// VStack(alignment: .leading) { +// Text(server.name!).font(.title3).fontWeight(.medium) +// if shouldDisplayDescription(server: server) { +// Spacer() +// Text(server.description!).opacity(0.6).font(.title3) +// } +// } +// } +// .padding() +// .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) +// .listRowSeparator(.hidden) +// .listRowBackground(Color(white: 0.96)) +// } + .popover(isPresented: $expanded) { + Text("Popover Content") + .padding() + } + .background(Color(white: 0.96)) + .padding(EdgeInsets(top: 8.0, leading: 24.0, bottom: 8.0, trailing: 24.0)) .presentationDetents([.fraction(0.4)]) - .presentationDragIndicator(.automatic) + .presentationDragIndicator(.visible) + .onTapGesture { + expanded = true +// withAnimation { +// expanded.toggle() +// } + } } } #Preview { TrackerServerView(server: HotlineServer(address: "192.168.1.1", port: 5050, users: 5, name: "Ye Olde Server", description: "This is a server")) .environment(HotlineClient()) + .environment(HotlineState()) } diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift index edb2330..83a4ebf 100644 --- a/Hotline/Views/TrackerView.swift +++ b/Hotline/Views/TrackerView.swift @@ -11,48 +11,67 @@ struct TrackerView: View { @State private var selectedServer: HotlineServer? + func shouldDisplayDescription(server: HotlineServer) -> Bool { + guard let name = server.name, let desc = server.description else { + return false + } + + return desc.count > 0 && desc != name && !desc.contains(/^-+/) + } + var body: some View { @Bindable var config = appState - NavigationView { - List(selection: $selectedServer) { - ForEach(tracker.servers) { server in - TrackerServerView(server: server) + List(selection: $selectedServer) { + ForEach(tracker.servers) { server in + NavigationLink { + ServerView(server: server) + } label: { + HStack(alignment: .firstTextBaseline) { + Text("🌎").font(.title3) + VStack(alignment: .leading) { + Text(server.name!).font(.title3).fontWeight(.medium) + if shouldDisplayDescription(server: server) { + Text(server.description!).opacity(0.6).font(.title3) + } + } + } } } - .background(Color(white: 0.96)) - .listStyle(.plain) - .frame(maxWidth: .infinity) - .task { - tracker.fetch() - } -// .sheet(item: $selectedServer) { item in -// TrackerServerView(server: item) -// } - .refreshable { - tracker.fetch() + } + .background(Color.white) + .listStyle(.plain) + .listRowSpacing(1) + .frame(maxWidth: .infinity) + .task { + tracker.fetch() + } + // .sheet(item: $selectedServer) { item in + //// Text("HELLO") + // TrackerServerView(server: item) + // } + .refreshable { + await withCheckedContinuation { continuation in + tracker.fetch() { + continuation.resume() + } } - .navigationTitle("Tracker") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - config.dismissTracker() - } label: { - Image(systemName: "xmark.circle.fill") - .symbolRenderingMode(.hierarchical) - .foregroundColor(.gray) - } + } + .navigationTitle("Tracker") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + config.dismissTracker() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.gray) } - // ToolbarItem(placement: .topBarTrailing) { - // Image(systemName: "camera.fill") - // } - // ToolbarItem(placement: .principal) { - // Text("Username") - // } } } -// .interactiveDismissDisabled() + + // .interactiveDismissDisabled() } } @@ -60,5 +79,6 @@ struct TrackerView: View { TrackerView() .environment(HotlineClient()) .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) + .environment(HotlineState()) // .modelContainer(for: Item.self, inMemory: true) } |