diff --git a/Beacon.xcodeproj/project.pbxproj b/Beacon.xcodeproj/project.pbxproj index 42a5c73..78f22ab 100644 --- a/Beacon.xcodeproj/project.pbxproj +++ b/Beacon.xcodeproj/project.pbxproj @@ -53,7 +53,6 @@ F3B1990E299518F900FE664F /* AutocompleteProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B19905299518F900FE664F /* AutocompleteProvider.swift */; }; F3B199142995195C00FE664F /* KeyboardKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3B199132995195C00FE664F /* KeyboardKit */; }; F3B199152995199000FE664F /* stdlib in Resources */ = {isa = PBXBuildFile; fileRef = F3B198DB2995181700FE664F /* stdlib */; }; - F3CEF79D2A94D3E000DC8858 /* KeyboardKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3CEF79C2A94D3E000DC8858 /* KeyboardKit */; }; F3CEF79E2A94D85500DC8858 /* Keyboard.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F3B198F1299518E900FE664F /* Keyboard.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F3D296822A404A3E00AD21A2 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D296812A404A3E00AD21A2 /* History.swift */; }; F3D296832A404A3E00AD21A2 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D296812A404A3E00AD21A2 /* History.swift */; }; @@ -64,6 +63,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + F31AB4F72AE0490F0074EA1B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F3B198C2299517DA00FE664F /* Project object */; + proxyType = 1; + remoteGlobalIDString = F3B198F0299518E900FE664F; + remoteInfo = Keyboard; + }; F3E1DFF02A31F56800B4A553 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F3B198C2299517DA00FE664F /* Project object */; @@ -159,7 +165,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F3CEF79D2A94D3E000DC8858 /* KeyboardKit in Frameworks */, F3DF828B2A4F22FF00F6CD19 /* libk.a in Frameworks */, F3AC4D262A4F6C3100B4FECD /* curl.xcframework in Frameworks */, F32D9E9629967067007BC97C /* libcbqn.a in Frameworks */, @@ -342,10 +347,10 @@ ); dependencies = ( F3E1DFF12A31F56800B4A553 /* PBXTargetDependency */, + F31AB4F82AE0490F0074EA1B /* PBXTargetDependency */, ); name = Beacon; packageProductDependencies = ( - F3CEF79C2A94D3E000DC8858 /* KeyboardKit */, ); productName = Beacon; productReference = F3B198CA299517DA00FE664F /* Beacon.app */; @@ -399,9 +404,7 @@ ); mainGroup = F3B198C1299517DA00FE664F; packageReferences = ( - F3B1990F2995193C00FE664F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, F3B199122995195C00FE664F /* XCRemoteSwiftPackageReference "KeyboardKit" */, - F3CEF79B2A94D3E000DC8858 /* XCRemoteSwiftPackageReference "KeyboardKit" */, ); productRefGroup = F3B198CB299517DA00FE664F /* Products */; projectDirPath = ""; @@ -506,6 +509,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + F31AB4F82AE0490F0074EA1B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F3B198F0299518E900FE664F /* Keyboard */; + targetProxy = F31AB4F72AE0490F0074EA1B /* PBXContainerItemProxy */; + }; F3E1DFF12A31F56800B4A553 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F3B198F0299518E900FE664F /* Keyboard */; @@ -594,6 +602,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -641,7 +650,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MERGEABLE_LIBRARY = NO; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -658,6 +667,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -699,7 +709,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MERGEABLE_LIBRARY = NO; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -720,7 +730,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Beacon/Preview Content\""; DEVELOPMENT_TEAM = KPW29M2DBH; ENABLE_PREVIEWS = YES; @@ -733,7 +743,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -744,7 +754,7 @@ "$(PROJECT_DIR)/Beacon/libs", "$(PROJECT_DIR)/Beacon/Libs", ); - MARKETING_VERSION = 1.1.5; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = arrscience.Beacons; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -767,7 +777,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Beacon/Preview Content\""; DEVELOPMENT_TEAM = KPW29M2DBH; ENABLE_PREVIEWS = YES; @@ -780,7 +790,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -791,7 +801,7 @@ "$(PROJECT_DIR)/Beacon/libs", "$(PROJECT_DIR)/Beacon/Libs", ); - MARKETING_VERSION = 1.1.5; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = arrscience.Beacons; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -908,14 +918,6 @@ kind = branch; }; }; - F3B1990F2995193C00FE664F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; - requirement = { - branch = master; - kind = branch; - }; - }; F3B199122995195C00FE664F /* XCRemoteSwiftPackageReference "KeyboardKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/KeyboardKit/KeyboardKit"; @@ -924,14 +926,6 @@ minimumVersion = 7.0.0; }; }; - F3CEF79B2A94D3E000DC8858 /* XCRemoteSwiftPackageReference "KeyboardKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/KeyboardKit/KeyboardKit"; - requirement = { - kind = exactVersion; - version = 7.9.0; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -945,11 +939,6 @@ package = F3B199122995195C00FE664F /* XCRemoteSwiftPackageReference "KeyboardKit" */; productName = KeyboardKit; }; - F3CEF79C2A94D3E000DC8858 /* KeyboardKit */ = { - isa = XCSwiftPackageProductDependency; - package = F3CEF79B2A94D3E000DC8858 /* XCRemoteSwiftPackageReference "KeyboardKit" */; - productName = KeyboardKit; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = F3B198C2299517DA00FE664F /* Project object */; diff --git a/Beacon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Beacon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0058ae..b515071 100644 --- a/Beacon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Beacon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,15 +8,6 @@ "revision" : "f6bd1def639641b2cff9ca4cddbb4615aa717e5c", "version" : "7.9.0" } - }, - { - "identity" : "swiftui-introspect", - "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", - "state" : { - "branch" : "master", - "revision" : "a5a1d7c305e9d9082958de04d521be57a88d431a" - } } ], "version" : 2 diff --git a/Beacon.xcodeproj/xcshareddata/xcschemes/KeyboardKit.xcscheme b/Beacon.xcodeproj/xcshareddata/xcschemes/KeyboardKit.xcscheme index 3bcc52d..2adeb84 100644 --- a/Beacon.xcodeproj/xcshareddata/xcschemes/KeyboardKit.xcscheme +++ b/Beacon.xcodeproj/xcshareddata/xcschemes/KeyboardKit.xcscheme @@ -1,6 +1,6 @@ ( + when shouldShow: Bool, + alignment: Alignment = .leading, + @ViewBuilder placeholder: () -> Content + ) -> some View { + ZStack(alignment: alignment) { + placeholder().opacity(shouldShow ? 1 : 0) + self + } + } +} diff --git a/Beacon/Models/History.swift b/Beacon/Models/History.swift index 775e244..eae2be6 100644 --- a/Beacon/Models/History.swift +++ b/Beacon/Models/History.swift @@ -8,11 +8,12 @@ import Foundation let d = UserDefaults.standard struct Entry: Hashable, Codable, Identifiable { - var id: String = UUID().uuidString + var id: UUID var src: String var out: String var lang: Language var tokens: [[Token]] + var isLoading: Bool } enum Buffers { @@ -40,11 +41,16 @@ enum Buffers { class HistoryModel: ObservableObject { @Published var history: [String: [Entry]] = ["default": []] - func addMessage(with src: String, out: String, lang: Language, for key: String) { + func addMessage(id: UUID, with src: String, out: String, lang: Language, for key: String, isLoading: Bool) { + if !isLoading { + if let index = history[key]?.firstIndex(where: { $0.id == id }) { + history[key]?.remove(at: index) + } + } let tokenize = lang == .k ? tokenize(src, lexK(src)) : tokenize(src, lexBQN(src)) - let entry = Entry(src: src, out: out, lang: lang, tokens: tokenize) + let entry = Entry(id: id, src: src, out: out, lang: lang, tokens: tokenize, isLoading: isLoading) if var entries = history[key] { entries.append(entry) history[key] = entries diff --git a/Beacon/Utilities/Lexer.swift b/Beacon/Utilities/Lexer.swift index 18012e5..38818e5 100644 --- a/Beacon/Utilities/Lexer.swift +++ b/Beacon/Utilities/Lexer.swift @@ -111,7 +111,7 @@ func lexBQN(_ code: String) -> [String] { let dmdC = "D" let dmd = "←↩,⋄→⇐" let comC = "C" - let newL = "E" + let _newL = "E" var res = Array(repeating: "", count: code.count) var i = code.startIndex diff --git a/Beacon/Utilities/Utils.swift b/Beacon/Utilities/Utils.swift index d0e0542..63be924 100644 --- a/Beacon/Utilities/Utils.swift +++ b/Beacon/Utilities/Utils.swift @@ -49,7 +49,7 @@ func kCmd(_ inp: UnsafePointer) { b.deallocate() } -func e(input: String) -> String { +func e(input: String) async -> String { var input = input if input.contains("•Import ") { let i = input.split(separator: "•Import ") @@ -58,11 +58,11 @@ func e(input: String) -> String { input = "\(vars) •Import \"\(Bundle.main.resourcePath!)/bqn-libs/\(filename)\"" } input = input.replacingOccurrences(of: "\"", with: #""""#) - input = "((•ReBQN{repl⇐\"loose\"})⎊{𝕊: •Out \"Error: \"∾•CurrentError@}) \"\(input)\"" + input = "RRR \"\(input)\"" return runCmd(cbqnCmd, input) } -func ke(input: String) -> String { +func ke(input: String) async -> String { var input = input.replacingOccurrences(of: "\\", with: #"\\"#) input = input.replacingOccurrences(of: "\"", with: #"\""#) input = ".[{line `k@.\"\(input)\"};[];{`0:(,\"Error: \"),(-2_\"\n\"\\x)}]" diff --git a/Beacon/Views/ContentView.swift b/Beacon/Views/ContentView.swift index 30db32a..653757c 100644 --- a/Beacon/Views/ContentView.swift +++ b/Beacon/Views/ContentView.swift @@ -19,12 +19,12 @@ struct ContentView: View { @State var curBuffer: String = "default" @State var inpPos: Int = -1 @State var move: (Int) -> Void = { _ in } - @AppStorage("lang") var lang: Language = .bqn - @AppStorage("editType") var editType: Behavior = .inlineEdit + @AppStorage("lang") var lang: Language = .k + @AppStorage("editType") var editType: Behavior = .duplicate @FocusState var isFocused: Bool @ObservedObject var viewModel: HistoryModel - func onMySubmit(input: String) { + func onMySubmit(input: String) async { switch input { case "clear": viewModel.clear(b: curBuffer) @@ -36,20 +36,24 @@ struct ContentView: View { } default: if !input.isEmpty { - // FIXME, below is a hacky workaround for appstorage not syncing? - let output = UserDefaults.standard.integer(forKey: "lang") == Language.bqn.rawValue - ? e(input: input) - : ke(input: input) + let key = UUID() + Task { + let output = await UserDefaults.standard.integer(forKey: "lang") == Language.bqn.rawValue + ? e(input: input) + : ke(input: input) - let attr = CSSearchableItemAttributeSet(contentType: .item) - attr.title = input - attr.contentDescription = output - attr.displayName = input - let uid = UUID().uuidString - let item = CSSearchableItem(uniqueIdentifier: uid, domainIdentifier: "arrscience.beacons", attributeSet: attr) - CSSearchableIndex.default().indexSearchableItems([item]) + let attr = CSSearchableItemAttributeSet(contentType: .item) + attr.title = input + attr.contentDescription = output + attr.displayName = input + let uid = UUID().uuidString + let item = CSSearchableItem(uniqueIdentifier: uid, domainIdentifier: "arrscience.beacons", attributeSet: attr) + try await CSSearchableIndex.default().indexSearchableItems([item]) + + viewModel.addMessage(id: key, with: input, out: output, lang: lang, for: curBuffer, isLoading: false) + } + viewModel.addMessage(id: key, with: input, out: "", lang: lang, for: curBuffer, isLoading: true) - viewModel.addMessage(with: input, out: output, lang: lang, for: curBuffer) } else { isFocused = false } @@ -61,7 +65,7 @@ struct ContentView: View { ScrollViewReader { scrollView in VStack { ScrollView(.vertical) { - VStack(spacing: 12) { + LazyVStack(spacing: 12) { if viewModel.history[curBuffer, default: []].isEmpty { VStack { Text("ngn/k, (c) 2019-2023") @@ -76,14 +80,16 @@ struct ContentView: View { HistoryView(index: index, historyItem: historyItem, curBuffer: curBuffer, onMySubmit: onMySubmit, input: $input, ephemerals: $ephemerals, editType: $editType, viewModel: viewModel) }.listRowBackground(Color.clear) .listRowSeparator(.hidden) + .pagedScrollingTarget() } }.id("HistoryScrollView") - .onChange(of: viewModel.history) { + .onChange(of: viewModel.history) { _ in withAnimation { scrollView.scrollTo("HistoryScrollView", anchor: .bottom) } } } + .scrolltargetbhv() }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) .padding(.bottom, 5) @@ -91,8 +97,8 @@ struct ContentView: View { helpOpen: $showHelp, settingsOpen: $showSettings, buffersOpen: $showBuffers, - lang: self.lang, onSubmit: { onMySubmit(input: self.input) }, - font: Font.custom("BQN386 Unicode", size: 20)) + lang: self.lang, onSubmit: { Task { await onMySubmit(input: self.input) } }, + font: Font.custom("BQN386 Unicode", size: 18)) .padding(.bottom, 4) .focused($isFocused) .onTapGesture { @@ -134,28 +140,64 @@ struct ContentView: View { repl_init() // below is adapted from https://codeberg.org/ngn/k/src/branch/master/repl.k let k_formatter = """ -(opn;sem;cls):"(;)" -lines:cols:80 -upd:(lines;cols) -lim:{(x<#y)(x#)/y} -dd:{(x<#y)(,[;".."](x-2)#)/y} -fmt:{upd[];$[(::)~x;"";(1<#x)&|/`m`M`A=@x;mMA x;(dd[cols]`k@lim[cols]x),"\n"]} -fmtx:{$[(::)~x;"";`k[x],"\n"]} -mMA:{(P;f;q):((,"!/+(";dct;,")");(("+![";" +(");tbl;")]");(,,"(";lst;,")"))`m`M`A?t:@x - w:cols-#*P;u:w-#q;h:lines-2 - x:$[h<(`M=t)+#x;,[;,".."](h-1)#f[w;u;h#x];f[w;u;x]] - ,[;"\n"]@"\n"/@[;-1+#x;,;q]P[!#x],'x} -lst:{[w;u;x](((-1+#x)#w),u)dd'`k'lim[cols]'x} -dct:{[w;u;x]k:(|/#'k)$k:`k'!x;par'(((-1+#x)#w-3),u-3)dd'sem/'+(k;`k'.x)} -tbl:{[w;u;x]h:`k'!x;d:`k''.x;W:(#'h)|/'#''d - r:,$[`S~@!x;dd[w](""opn),(""sem)/;par@dd[w-2]@sem/]W$'h - r,par'dd[w-2]'sem/'+@[W;&~^`i`d?_@'.x;-:]$'} -cell:{$[|/`i`d=@y;-x;x]$z} -par:{opn,x,cls} -line0:{c:{0x07~*-2#*x}{(l;r):x;(1:1;r,,(-2_l))}/(x;());"\n"/(*|c),,*c} -line1:{$[#x;;:0];.[`1:(fmt;fmtx)[" "~*x]@.:;,x;{`0:`err[]}]} -line:line1@line0@ -""" + (opn;sem;cls):"(;)" + lines:cols:80 + upd:(lines;cols) + lim:{(x<#y)(x#)/y} + dd:{(x<#y)(,[;".."](x-2)#)/y} + fmt:{upd[];$[(::)~x;"";(1<#x)&|/`m`M`A=@x;mMA x;(dd[cols]`k@lim[cols]x),"\n"]} + fmtx:{$[(::)~x;"";`k[x],"\n"]} + mMA:{(P;f;q):((,"!/+(";dct;,")");(("+![";" +(");tbl;")]");(,,"(";lst;,")"))`m`M`A?t:@x + w:cols-#*P;u:w-#q;h:lines-2 + x:$[h<(`M=t)+#x;,[;,".."](h-1)#f[w;u;h#x];f[w;u;x]] + ,[;"\n"]@"\n"/@[;-1+#x;,;q]P[!#x],'x} + lst:{[w;u;x](((-1+#x)#w),u)dd'`k'lim[cols]'x} + dct:{[w;u;x]k:(|/#'k)$k:`k'!x;par'(((-1+#x)#w-3),u-3)dd'sem/'+(k;`k'.x)} + tbl:{[w;u;x]h:`k'!x;d:`k''.x;W:(#'h)|/'#''d + r:,$[`S~@!x;dd[w](""opn),(""sem)/;par@dd[w-2]@sem/]W$'h + r,par'dd[w-2]'sem/'+@[W;&~^`i`d?_@'.x;-:]$'} + cell:{$[|/`i`d=@y;-x;x]$z} + par:{opn,x,cls} + line0:{c:{0x07~*-2#*x}{(l;r):x;(1:1;r,,(-2_l))}/(x;());"\n"/(*|c),,*c} + line1:{$[#x;;:0];.[`1:(fmt;fmtx)[" "~*x]@.:;,x;{`0:`err[]}]} + line:line1@line0@ + """ let _ = runCmd(kCmd, k_formatter) + let bqnRepl = "RRR←((•ReBQN{repl⇐\"loose\"})⎊{𝕊: •Out \"Error: \"∾•CurrentError@})" + let _ = runCmd(cbqnCmd, bqnRepl) + } +} + +struct PagedScrollingTarget: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content + .scrollTargetLayout() + } else { + content + } + } +} + +extension View { + func pagedScrollingTarget() -> some View { + modifier(PagedScrollingTarget()) + } +} + +struct ScrollTargetBhv: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 17.0, *) { + content + .scrollTargetBehavior(.viewAligned) + } else { + content + } + } +} + +extension View { + func scrolltargetbhv() -> some View { + modifier(ScrollTargetBhv()) } } diff --git a/Beacon/Views/DashboardView.swift b/Beacon/Views/DashboardView.swift index 57b22ed..87bef5d 100644 --- a/Beacon/Views/DashboardView.swift +++ b/Beacon/Views/DashboardView.swift @@ -3,263 +3,265 @@ // Arrayground // -import Charts -import Combine -import Foundation -import SwiftUI +/* + import Charts + import Combine + import Foundation + import SwiftUI -struct DataElement: Identifiable, Comparable { - var id = UUID() - let key: String - let value: Int + struct DataElement: Identifiable, Comparable { + var id = UUID() + let key: String + let value: Int - static func < (lhs: DataElement, rhs: DataElement) -> Bool { - return lhs.key < rhs.key - } + static func < (lhs: DataElement, rhs: DataElement) -> Bool { + return lhs.key < rhs.key + } - static func == (lhs: DataElement, rhs: DataElement) -> Bool { - return lhs.key == rhs.key - } -} + static func == (lhs: DataElement, rhs: DataElement) -> Bool { + return lhs.key == rhs.key + } + } -struct Card: Identifiable, Codable { - let id: UUID - var snippet: String -} + struct Card: Identifiable, Codable { + let id: UUID + var snippet: String + } -func parseData(s: String) -> [String: Int] { - if s != "" { - let components = s.components(separatedBy: "!") - let keysString = components[0].trimmingCharacters(in: CharacterSet(charactersIn: "()\"")) - let valuesString = components[1] - let keys = keysString.components(separatedBy: ";") - let values = valuesString.split(separator: " ").map { Int($0) } - assert(keys.count == values.count, "Keys and values count mismatch!") - var dict: [String: Int] = [:] - for (index, key) in keys.enumerated() { - dict[key.replacingOccurrences(of: "\"", with: #""#)] = values[index] - } - return dict - } else { - return [:] - } -} + func parseData(s: String) -> [String: Int] { + if s != "" { + let components = s.components(separatedBy: "!") + let keysString = components[0].trimmingCharacters(in: CharacterSet(charactersIn: "()\"")) + let valuesString = components[1] + let keys = keysString.components(separatedBy: ";") + let values = valuesString.split(separator: " ").map { Int($0) } + assert(keys.count == values.count, "Keys and values count mismatch!") + var dict: [String: Int] = [:] + for (index, key) in keys.enumerated() { + dict[key.replacingOccurrences(of: "\"", with: #""#)] = values[index] + } + return dict + } else { + return [:] + } + } -class DashboardViewModel: ObservableObject { - @Published var cards: [Card] = [] { - didSet { - let encoder = JSONEncoder() - if let encoded = try? encoder.encode(cards) { - UserDefaults.standard.set(encoded, forKey: "cards") - } - } - } + class DashboardViewModel: ObservableObject { + @Published var cards: [Card] = [] { + didSet { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(cards) { + UserDefaults.standard.set(encoded, forKey: "cards") + } + } + } - func removeCard(at index: Int) { - cards.remove(at: index) - } + func removeCard(at index: Int) { + cards.remove(at: index) + } - init() { - if let cardsData = UserDefaults.standard.data(forKey: "cards") { - let decoder = JSONDecoder() - if let loadedCards = try? decoder.decode([Card].self, from: cardsData) { - cards = loadedCards - return - } - } - cards = [Card(id: UUID(), snippet: "")] - } -} + init() { + if let cardsData = UserDefaults.standard.data(forKey: "cards") { + let decoder = JSONDecoder() + if let loadedCards = try? decoder.decode([Card].self, from: cardsData) { + cards = loadedCards + return + } + } + cards = [Card(id: UUID(), snippet: "")] + } + } -struct Dashboard: View { - @StateObject var viewModel = DashboardViewModel() - var body: some View { - VStack { - Text("Dashboard") - .font(.largeTitle) - .fontWeight(.bold) - .padding(.bottom, 20) - Spacer() - ScrollView(.vertical, showsIndicators: false) { - VStack { - ForEach(viewModel.cards.indices, id: \.self) { index in - CardView(card: $viewModel.cards[index], removeCard: { - viewModel.removeCard(at: index) - }) - .padding(.horizontal) - } + struct Dashboard: View { + @StateObject var viewModel = DashboardViewModel() + var body: some View { + VStack { + Text("Dashboard") + .font(.largeTitle) + .fontWeight(.bold) + .padding(.bottom, 20) + Spacer() + ScrollView(.vertical, showsIndicators: false) { + VStack { + ForEach(viewModel.cards.indices, id: \.self) { index in + CardView(card: $viewModel.cards[index], removeCard: { + viewModel.removeCard(at: index) + }) + .padding(.horizontal) + } - Button(action: { - viewModel.cards.append(Card(id: UUID(), snippet: "")) - }) { - HStack { Image(systemName: "plus") } - .padding() - .foregroundColor(.white) - .background(Color.blue) - .cornerRadius(10) - } - .padding(.horizontal) - } - } - Spacer() - } - .padding() - } -} + Button(action: { + viewModel.cards.append(Card(id: UUID(), snippet: "")) + }) { + HStack { Image(systemName: "plus") } + .padding() + .foregroundColor(.white) + .background(Color.blue) + .cornerRadius(10) + } + .padding(.horizontal) + } + } + Spacer() + } + .padding() + } + } -struct CardView: View { - @Binding var card: Card - let removeCard: () -> Void - @State private var barSelection: String? - @State private var isLoading: Bool = false - @State private var isEditing: Bool = false - @State var output: String = "" - @State var snippet: String = "" - var refreshRate: Double = 60.0 - var data: [String: Int] { return parseData(s: output) } - let timer = Timer.publish(every: 15, on: .main, in: .common).autoconnect() + struct CardView: View { + @Binding var card: Card + let removeCard: () -> Void + @State private var barSelection: String? + @State private var isLoading: Bool = false + @State private var isEditing: Bool = false + @State var output: String = "" + @State var snippet: String = "" + var refreshRate: Double = 60.0 + var data: [String: Int] { return parseData(s: output) } + let timer = Timer.publish(every: 15, on: .main, in: .common).autoconnect() - var body: some View { - VStack { - ZStack { - VStack { - HStack { - Spacer() - Button(action: { self.removeCard() }) { - Image(systemName: "xmark.circle.fill") - .font(.title2) - .foregroundColor(.red) - } - Button(action: { - self.isEditing = true - }) { - Image(systemName: "pencil.circle.fill") - .font(.title2) - .foregroundColor(.green) - } - .padding(.trailing) - } - if self.isEditing || self.card.snippet.isEmpty { - TextEditor(text: $snippet) - .padding(.horizontal) - .navigationTitle("snippet") - .onChange(of: snippet) { - if snippet.suffix(2) == "\n\n" { - self.snippet = self.snippet.trimmingLastOccurrence(of: "\n\n") - self.card.snippet = self.snippet - self.isEditing = false - self.refresh() - } - } - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(Font.system(.body, design: .monospaced)) - .foregroundColor(.accentColor) // Text color - .frame(width: 350, height: 350) - .border(.yellow) - .padding() - } else { - ZStack { - if isLoading { - ProgressView().frame(width: 350, height: 350) - } - VStack { - VStack { - if !output.contains("Error") && output.contains("!") { // FIXME: need a better check if the output is a dict - Chart { - ForEach(data.map { DataElement(key: $0.key, value: $0.value) }.sorted()) { item in - BarMark( - x: .value("Key", item.key), - y: .value("Count", item.value) - ) - .cornerRadius(8) - .foregroundStyle(by: .value("Result Color", item.key)) - } - if let barSelection { - RuleMark(x: .value("Key", barSelection)) - .foregroundStyle(.gray.opacity(0.35)) - .zIndex(-10) - .offset(yStart: -15) - .annotation( - position: .top, - spacing: 0, - overflowResolution: .init(x: .disabled, y: .disabled) - ) { - ChartPopOverView(xval: barSelection, yval: data[barSelection] ?? 0) - } - } - } - .chartXSelection(value: $barSelection) - .chartLegend(position: .bottom, alignment: .leading, spacing: 25) - .padding(.all, 15) - .chartYAxis { - AxisMarks(position: .leading) { _ in - AxisValueLabel() - } - } - .chartXAxis { - AxisMarks(position: .bottom) { _ in - AxisValueLabel() - } - } - } else { - Text(output) - .font(.body) - .padding() - Spacer() - } - /* - ProgressView(value: updater.timeRemaining, total: updater.refreshRate) - .accentColor(.green) - .frame(height: 8.0) - .scaleEffect(x: 1, y: 2, anchor: .center) - */ - } - .frame(width: 350, height: 350) - } - } - // .border(.blue) - } - } - .padding() - } - }.onAppear(perform: { - self.snippet = card.snippet - refresh() - }).onReceive(timer) { _ in - refresh() - } - } + var body: some View { + VStack { + ZStack { + VStack { + HStack { + Spacer() + Button(action: { self.removeCard() }) { + Image(systemName: "xmark.circle.fill") + .font(.title2) + .foregroundColor(.red) + } + Button(action: { + self.isEditing = true + }) { + Image(systemName: "pencil.circle.fill") + .font(.title2) + .foregroundColor(.green) + } + .padding(.trailing) + } + if self.isEditing || self.card.snippet.isEmpty { + TextEditor(text: $snippet) + .padding(.horizontal) + .navigationTitle("snippet") + .onChange(of: snippet) { + if snippet.suffix(2) == "\n\n" { + self.snippet = self.snippet.trimmingLastOccurrence(of: "\n\n") + self.card.snippet = self.snippet + self.isEditing = false + self.refresh() + } + } + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(Font.system(.body, design: .monospaced)) + .foregroundColor(.accentColor) // Text color + .frame(width: 350, height: 350) + .border(.yellow) + .padding() + } else { + ZStack { + if isLoading { + ProgressView().frame(width: 350, height: 350) + } + VStack { + VStack { + if !output.contains("Error") && output.contains("!") { // FIXME: need a better check if the output is a dict + Chart { + ForEach(data.map { DataElement(key: $0.key, value: $0.value) }.sorted()) { item in + BarMark( + x: .value("Key", item.key), + y: .value("Count", item.value) + ) + .cornerRadius(8) + .foregroundStyle(by: .value("Result Color", item.key)) + } + if let barSelection { + RuleMark(x: .value("Key", barSelection)) + .foregroundStyle(.gray.opacity(0.35)) + .zIndex(-10) + .offset(yStart: -15) + .annotation( + position: .top, + spacing: 0, + overflowResolution: .init(x: .disabled, y: .disabled) + ) { + ChartPopOverView(xval: barSelection, yval: data[barSelection] ?? 0) + } + } + } + .chartXSelection(value: $barSelection) + .chartLegend(position: .bottom, alignment: .leading, spacing: 25) + .padding(.all, 15) + .chartYAxis { + AxisMarks(position: .leading) { _ in + AxisValueLabel() + } + } + .chartXAxis { + AxisMarks(position: .bottom) { _ in + AxisValueLabel() + } + } + } else { + Text(output) + .font(.body) + .padding() + Spacer() + } + /* + ProgressView(value: updater.timeRemaining, total: updater.refreshRate) + .accentColor(.green) + .frame(height: 8.0) + .scaleEffect(x: 1, y: 2, anchor: .center) + */ + } + .frame(width: 350, height: 350) + } + } + // .border(.blue) + } + } + .padding() + } + }.onAppear(perform: { + self.snippet = card.snippet + refresh() + }).onReceive(timer) { _ in + refresh() + } + } - func refresh() { - if !isEditing { - Task.init { - self.isLoading = true - let snippets = snippet.split(separator: "\n") - for snippet in snippets { - let to = UserDefaults.standard.integer(forKey: "lang") == Language.bqn.rawValue - ? e(input: String(snippet)) - : ke(input: String(snippet)) - if snippet == snippets.last { - self.output = to - } - } - self.isLoading = false - } - } - } + func refresh() { + if !isEditing { + Task.init { + self.isLoading = true + let snippets = snippet.split(separator: "\n") + for snippet in snippets { + let to = await UserDefaults.standard.integer(forKey: "lang") == Language.bqn.rawValue + ? e(input: String(snippet)) + : ke(input: String(snippet)) + if snippet == snippets.last { + self.output = to + } + } + self.isLoading = false + } + } + } - @ViewBuilder - func ChartPopOverView(xval: String, yval: Int) -> some View { - VStack(alignment: .leading, spacing: 6) { - HStack(spacing: 4) { - Text(String(yval)) - .font(.title3) - Text(xval) - .font(.title3) - .textScale(.secondary) - } - } - .padding() - .background(.cyan, in: .rect(cornerRadius: 8)) - } -} + @ViewBuilder + func ChartPopOverView(xval: String, yval: Int) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 4) { + Text(String(yval)) + .font(.title3) + Text(xval) + .font(.title3) + .textScale(.secondary) + } + } + .padding() + .background(.cyan, in: .rect(cornerRadius: 8)) + } + } + */ diff --git a/Beacon/Views/HistoryView.swift b/Beacon/Views/HistoryView.swift index 5db421a..4b33ef6 100644 --- a/Beacon/Views/HistoryView.swift +++ b/Beacon/Views/HistoryView.swift @@ -10,13 +10,14 @@ struct HistoryView: View { var index: Int var historyItem: Entry var curBuffer: String - var onMySubmit: (String) -> Void + var onMySubmit: (String) async -> Void @Binding var input: String @Binding var ephemerals: [Int: [String]] @Binding var editType: Behavior @ObservedObject var viewModel: HistoryModel @Environment(\.colorScheme) var scheme: ColorScheme - + @State private var isShowingCard = false + var body: some View { VStack(alignment: .leading) { if editType == Behavior.inlineEdit { @@ -36,11 +37,14 @@ struct HistoryView: View { } )) .onSubmit { - onMySubmit(self.viewModel.history[curBuffer, default: []][index].src) - for k in ephemerals.keys { - self.viewModel.history[curBuffer, default: []][k].src = ephemerals[k, default: []].first! + Task { + let src = self.viewModel.history[curBuffer, default: []][index].src + await onMySubmit(src) + for k in ephemerals.keys { + self.viewModel.history[curBuffer, default: []][k].src = ephemerals[k, default: []].first! + } + ephemerals = [:] // reset all virtual textfield edits } - ephemerals = [:] // reset all virtual textfield edits } .font(Font.custom("BQN386 Unicode", size: 18)) .foregroundColor(.blue) @@ -57,20 +61,41 @@ struct HistoryView: View { .font(Font.custom("BQN386 Unicode", size: 18)) .onTapGesture { self.input = historyItem.src + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() } } }.frame(maxWidth: .infinity, alignment: .leading) + .onLongPressGesture(minimumDuration: 0.5) { + withAnimation { + self.isShowingCard = true + } + } } } - } - Text("\(trimLongText(historyItem.out))") - .foregroundStyle(.primary.opacity(0.8)) - .font(Font.custom("BQN386 Unicode", size: 18)) - .foregroundColor(historyItem.out.starts(with: "Error:") || historyItem.out.starts(with: "\"Error:") ? .red : .primary) - .multilineTextAlignment(.leading) - .onTapGesture { - self.input = historyItem.out + .popover(isPresented: $isShowingCard) { + Text("selectedLine") + .padding() + .frame(width: 200, height: 100) + .background(Color.white) + .cornerRadius(10) + .shadow(radius: 5) } + } + if historyItem.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(0.5, anchor: .center) + } else { + Text("\(trimLongText(historyItem.out))") + .foregroundStyle(.primary.opacity(0.8)) + .font(Font.custom("BQN386 Unicode", size: 18)) + .foregroundColor(historyItem.out.starts(with: "Error:") || historyItem.out.starts(with: "\"Error:") ? .red : .primary) + .multilineTextAlignment(.leading) + .onTapGesture { + self.input = historyItem.out + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + } + } }.frame(maxWidth: .infinity, alignment: .leading) } }