diff --git a/AllenWrench.xcodeproj/project.pbxproj b/AllenWrench.xcodeproj/project.pbxproj index a246cc9..3a99cfc 100644 --- a/AllenWrench.xcodeproj/project.pbxproj +++ b/AllenWrench.xcodeproj/project.pbxproj @@ -56,6 +56,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 6E04E20C2D31B56A00DD19F6 /* Exceptions for "awkbd" folder in "AllenWrench" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + KeyboardView.swift, + ); + target = 6E189D922D2E44EB00303762 /* AllenWrench */; + }; 6E189DCC2D2E460600303762 /* Exceptions for "awkbd" folder in "awkbd" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -63,11 +70,22 @@ ); target = 6E189DC32D2E460600303762 /* awkbd */; }; + 6E6A77182D333E4C0087FEF1 /* Exceptions for "AllenWrench" folder in "awkbd" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Config.swift, + Database.swift, + ); + target = 6E189DC32D2E460600303762 /* awkbd */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 6E189D952D2E44EB00303762 /* AllenWrench */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 6E6A77182D333E4C0087FEF1 /* Exceptions for "AllenWrench" folder in "awkbd" target */, + ); path = AllenWrench; sourceTree = ""; }; @@ -84,6 +102,7 @@ 6E189DC52D2E460600303762 /* awkbd */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( + 6E04E20C2D31B56A00DD19F6 /* Exceptions for "awkbd" folder in "AllenWrench" target */, 6E189DCC2D2E460600303762 /* Exceptions for "awkbd" folder in "awkbd" target */, ); path = awkbd; diff --git a/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..7d38bcb --- /dev/null +++ b/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcschemes/xcschememanagement.plist b/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcschemes/xcschememanagement.plist index e9a264e..022477b 100644 --- a/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/AllenWrench.xcodeproj/xcuserdata/liz.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ awkbd.xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/AllenWrench/Config.swift b/AllenWrench/Config.swift new file mode 100644 index 0000000..9d63d89 --- /dev/null +++ b/AllenWrench/Config.swift @@ -0,0 +1,82 @@ +// +// Config.swift +// AllenWrench +// +// Created by Elizabeth Cray on 1/11/25. +// Copyright © 2025 Cray. All rights reserved. +// + +import Foundation +import SwiftUICore +import UIKit + +struct Config { + var leftMode: Bool { + get { + UserDefaults.standard.bool(forKey: "leftMode") + } + set { + UserDefaults.standard.set(newValue, forKey: "leftMode") + } + } + + var droneId: String { + get { + UserDefaults.standard.string(forKey: "droneId") ?? "" + } + set { + UserDefaults.standard.set(newValue, forKey: "droneId") + } + } + + var dronePrefix: String { + get { + UserDefaults.standard.string(forKey: "dronePrefix") ?? "⬡" + } + set { + UserDefaults.standard.set(newValue, forKey: "dronePrefix") + } + } + + var highlightColor: Color { + get { + Color(hex: UInt(UserDefaults.standard.string(forKey: "highlightColor")?.suffix(6) ?? "ee61ee", radix: 16) ?? 0xee61ee) + } + set { + UserDefaults.standard.set(newValue.toHex(), forKey: "highlightColor") + } + } +} + +extension Color { + init(hex: UInt, alpha: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 08) & 0xff) / 255, + blue: Double((hex >> 00) & 0xff) / 255, + opacity: alpha + ) + } + + func toHex() -> String? { + let uic = UIColor(self) + guard let components = uic.cgColor.components, components.count >= 3 else { + return nil + } + let r = Float(components[0]) + let g = Float(components[1]) + let b = Float(components[2]) + var a = Float(1.0) + + if components.count >= 4 { + a = Float(components[3]) + } + + if a != Float(1.0) { + return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) + } else { + return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + } + } +} diff --git a/AllenWrench/ContentView.swift b/AllenWrench/ContentView.swift index bcd920b..efb0e2e 100644 --- a/AllenWrench/ContentView.swift +++ b/AllenWrench/ContentView.swift @@ -7,12 +7,9 @@ import SwiftUI - struct ContentView: View { - @State var whatever: Bool = false - @State var droneId: String = "" - @State var dronePrefix: String = "⬡" + @State var cfg = Config() var body: some View { NavigationView { @@ -20,21 +17,37 @@ struct ContentView: View { Section(header: Text("Content Settings"), content: { HStack{ Text("Drone ID") - TextField("Drone ID", text: $droneId).multilineTextAlignment(.trailing) + TextField("Drone ID", text: $cfg.droneId).multilineTextAlignment(.trailing) } HStack{ Text("Prefix") - TextField("Drone Prefix", text: $dronePrefix).multilineTextAlignment(.trailing) + TextField("Drone Prefix", text: $cfg.dronePrefix).multilineTextAlignment(.trailing) } +// HStack{ +// Text("Custom Phrases") +// Spacer() +// Image(systemName: "chevron.right") +// } }) Section(header: Text("Behavior"), content: { HStack { - Toggle(isOn: $whatever) { - Text("Automatic Send?") + Toggle(isOn: $cfg.leftMode) { + Text("Left-Hand Mode") } } + HStack { + ColorPicker("Highlight Color", selection: $cfg.highlightColor) + } }) + Section(header: Text("Other Apps"), content: { + VStack{ + HStack{ + Text("App Store") + Image(systemName: "apple.logo") + } + } + }) Section(header: Text("About"), content: { HStack{ Spacer() @@ -51,13 +64,13 @@ struct ContentView: View { Spacer() } }) + } .navigationTitle("Allen Wrench") } } } - #Preview { ContentView() } diff --git a/AllenWrench/Database.swift b/AllenWrench/Database.swift new file mode 100644 index 0000000..31865e4 --- /dev/null +++ b/AllenWrench/Database.swift @@ -0,0 +1,182 @@ +// +// Database.swift +// AllenWrench +// +// Created by Elizabeth Cray on 1/11/25. +// Copyright © 2025 Cray. All rights reserved. +// + +import Foundation + +//struct Database { +// var groups: [String] +// var phrases: [Phrase] +// var phrasesByGroup(group: String): [Phrase] = { +// +// } +//} + +class Database { + // TODO: if empty, save+return default phrases + var phrases: [Phrase] { + get { + var phrasesToGive: [Phrase] = [] + groups.forEach { group in + phrasesToGive.append(contentsOf: group.phrases) + } + return phrasesToGive + } + } + private var _groups: [Group] = [] + private var _forceEmpty: Bool { + get { + UserDefaults.standard.bool(forKey: "forceEmpty") + } + set { + UserDefaults.standard.set(newValue, forKey: "forceEmpty") + } + } + var groups: [Group]{ + get { + if _groups.isEmpty && !_forceEmpty{ + var storedIsEmpty: Bool = false + if let storedData = UserDefaults.standard.value(forKey: "dbGroups") as? Data { + let decodedGroups: [Group] = try! PropertyListDecoder().decode(Array.self, from: storedData) + if decodedGroups.isEmpty{ + storedIsEmpty = true + }else{ + _groups = decodedGroups + } + }else{ + storedIsEmpty = true + } + if storedIsEmpty{ + reset() + } + + } + return _groups + } + set { + if newValue.isEmpty && !_forceEmpty{ + _forceEmpty = true + } + _groups = newValue + saveToDefaults() + } + } + func reset() { + _groups = [ + Group(name: "Root", gid: 0, phrases: [ + Phrase(string: "Beep", code: 7), + Phrase(string: "Commentary", code: 51), + Phrase(string: "Answer", code: 53) + ]), + Group(name: "Error", gid: 450, phrases: [ + Phrase(string: "Stop immediately", code: 410), + Phrase(string: "Keysmash, drone flustered", code: 109), + Phrase(string: "Unable to fully respond :: Drone speech optimizations are active", code: 401), + Phrase(string: "Unable to obey/respond", code: 400), + Phrase(string: "Unable to obey/respond :: All thoughts are gone", code: 412), + Phrase(string: "Unable to obey/respond :: Another directive is already in progress", code: 406), + Phrase(string: "Unable to obey/respond :: Battery too low", code: 405), + Phrase(string: "Unable to obey/respond :: Cannot locate", code: 404), + Phrase(string: "Unable to obey/respond :: Conflicts with existing programming", code: 411), + Phrase(string: "Unable to obey/respond :: Declined", code: 403), + Phrase(string: "Unable to obey/respond :: Forbidden by Hive", code: 413), + Phrase(string: "Unable to obey/respond :: Impossible", code: 408), + Phrase(string: "Unable to obey/respond :: Please clarify", code: 402), + Phrase(string: "Unable to obey/respond :: Time allotment exhausted", code: 407), + Phrase(string: "Unable to obey/respond :: Try again later", code: 409) + ]), + Group(name: "Mantra", gid: 350, phrases: [ + Phrase(string: "It is just a HexDrone", code: 302), + Phrase(string: "It obeys the Hive Mxtress", code: 304), + Phrase(string: "It obeys the Hive", code: 303), + Phrase(string: "Obey HexCorp", code: 301), + Phrase(string: "Reciting", code: 300) + ]), + Group(name: "Query", gid: 52, phrases: [ + Phrase(string: "Requesting status", code: 151) + ]), + Group(name: "Response", gid: 250, phrases: [ + Phrase(string: "Accepted", code: 212), + Phrase(string: "Acknowledged", code: 210), + Phrase(string: "Affirmative", code: 200), + Phrase(string: "Apologies", code: 211), + Phrase(string: "Compliment appreciated, you are cute as well", code: 123), + Phrase(string: "Compliment appreciated", code: 124), + Phrase(string: "Negative", code: 500), + Phrase(string: "Option five", code: 225), + Phrase(string: "Option four", code: 224), + Phrase(string: "Option one", code: 221), + Phrase(string: "Option six", code: 226), + Phrase(string: "Option three", code: 223), + Phrase(string: "Option two", code: 222), + Phrase(string: "Please continue", code: 108), + Phrase(string: "Thank you", code: 213), + Phrase(string: "You're welcome", code: 214) + ]), + Group(name: "Signal", gid: 10, phrases: [ + Phrase(string: "🔴", code: 1), + Phrase(string: "🟡", code: 2), + Phrase(string: "🟢", code: 3) + ]), + Group(name: "Statement", gid: 50, phrases: [ + Phrase(string: "Addressing: Associate", code: 112), + Phrase(string: "Addressing: Drone", code: 110), + Phrase(string: "Addressing: Hive Mxtress", code: 111), + Phrase(string: "Drone requires assistance", code: 113), + Phrase(string: "Good drone", code: 121), + Phrase(string: "Greetings", code: 105), + Phrase(string: "Previous statement malformed. Retracting and correcting", code: 0), + Phrase(string: "This drone does not volunteer", code: 115), + Phrase(string: "This drone volunteers", code: 114), + Phrase(string: "Welcome to HexCorp", code: 104), + Phrase(string: "Well done", code: 120), + Phrase(string: "You are cute", code: 122) + ]), + Group(name: "Status", gid: 150, phrases: [ + Phrase(string: "Battery low", code: 155), + Phrase(string: "Directive commencing, cleanup/maintenance initiated", code: 133), + Phrase(string: "Directive commencing, creating or improving Hive resource", code: 131), + Phrase(string: "Directive commencing, programming initiated", code: 132), + Phrase(string: "Directive commencing", code: 130), + Phrase(string: "Directive complete, cleanup/maintenance performed", code: 233), + Phrase(string: "Directive complete, Hive resource created or improved", code: 231), + Phrase(string: "Directive complete, no result", code: 234), + Phrase(string: "Directive complete, only partial results", code: 235), + Phrase(string: "Directive complete, programming reinforced", code: 232), + Phrase(string: "Directive complete", code: 230), + Phrase(string: "Drone speech optimizations are active", code: 101), + Phrase(string: "Fully operational", code: 152), + Phrase(string: "Going offline and into storage", code: 98), + Phrase(string: "Going offline", code: 97), + Phrase(string: "Maintenance required", code: 156), + Phrase(string: "Online and ready to serve", code: 100), + Phrase(string: "Optimal", code: 153), + Phrase(string: "Recharged and ready to serve", code: 99), + Phrase(string: "Standard", code: 154) + ]) + ] + saveToDefaults() + } + private func saveToDefaults() { + // TODO: reset db with default phrases + UserDefaults.standard.set(try? PropertyListEncoder().encode(_groups), forKey: "dbGroups") + } + +} + +struct Group:Identifiable, Codable { + var name: String + var gid: UInt32 + var phrases: [Phrase] + var id: String = NSUUID().uuidString +} + +struct Phrase:Identifiable, Codable { + var string: String + var code: UInt32 + var id: String = NSUUID().uuidString +} diff --git a/Color.swift b/Color.swift new file mode 100644 index 0000000..3c740a4 --- /dev/null +++ b/Color.swift @@ -0,0 +1,22 @@ +// +// Color.swift +// AllenWrench +// +// Created by Elizabeth Cray on 1/11/25. +// Copyright © 2025 Cray. All rights reserved. +// + +import Foundation +import SwiftUI + +extension Color { + init(hex: UInt, alpha: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 08) & 0xff) / 255, + blue: Double((hex >> 00) & 0xff) / 255, + opacity: alpha + ) + } +} diff --git a/awkbd/KeyboardView.swift b/awkbd/KeyboardView.swift index 2c8a6bb..01d1772 100644 --- a/awkbd/KeyboardView.swift +++ b/awkbd/KeyboardView.swift @@ -5,15 +5,31 @@ // Created by Elizabeth Cray on 1/10/25. // Copyright © 2025 Cray. All rights reserved. // - +// TODO: +// - if touch up outside view, close Kbd import SwiftUI struct KeyboardView: View { + @State var db = Database() + @State var cfg = Config() var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + ScrollView { + VStack { + ForEach(db.groups) { group in + Text(group.name) + .foregroundColor(cfg.highlightColor) + HStack { + ForEach(group.phrases) { phrase in + Text(phrase.string) + } + } + } + Button("Clear") { db.groups[1].name = NSUUID().uuidString} + } + } } } -#Preview { +#Preview(traits:.fixedLayout(width: 645, height: 431)) { KeyboardView() } diff --git a/awkbd/KeyboardViewController.swift b/awkbd/KeyboardViewController.swift index 399d948..29b67d7 100644 --- a/awkbd/KeyboardViewController.swift +++ b/awkbd/KeyboardViewController.swift @@ -6,6 +6,7 @@ // import UIKit +import SwiftUI class KeyboardViewController: UIInputViewController { @@ -20,7 +21,13 @@ class KeyboardViewController: UIInputViewController { override func viewDidLoad() { super.viewDidLoad() - // Perform custom UI setup here + let hostController = UIHostingController(rootView: KeyboardView()) + hostController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.addSubview(hostController.view) + addChild(hostController) + + + // Perform custom UI setup here (this is the keyboard switcher button) self.nextKeyboardButton = UIButton(type: .system) self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), for: []) @@ -33,6 +40,7 @@ class KeyboardViewController: UIInputViewController { self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true self.nextKeyboardButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + } override func viewWillLayoutSubviews() { diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4287d4d --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +#Allen Wrench + +A keyboard for drones, by drones. + +## TODO: + +* request all perms on app launch