diff --git a/freewrite/AudioManager.swift b/freewrite/AudioManager.swift new file mode 100644 index 0000000..d927534 --- /dev/null +++ b/freewrite/AudioManager.swift @@ -0,0 +1,77 @@ +import AVFoundation + +struct KeySound { + let startTime: Double + let duration: Double +} + +class AudioManager { + static let shared = AudioManager() + private var soundData: Data? + private var keySoundMap: [String: KeySound] = [:] + private(set) var isEnabled: Bool = false + + private init() { + setupKeyboardSound() + loadKeyDefinitions() + } + + private func setupKeyboardSound() { + if let soundURL = Bundle.main.url(forResource: "crystal_purple", withExtension: "mp3") { + loadSound(from: soundURL) + } + } + + private func loadSound(from url: URL) -> Void { + do { + soundData = try Data(contentsOf: url) + } catch {} + } + + private func loadKeyDefinitions() { + if let configURL = Bundle.main.url(forResource: "crystal_purple_config", withExtension: "json") { + loadConfig(from: configURL) + } + } + + private func loadConfig(from url: URL) { + do { + let data = try Data(contentsOf: url) + let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + if let defines = json?["defines"] as? [String: [Double]] { + for (key, value) in defines { + if value.count == 2 { + let startTime = value[0] / 1000.0 + let duration = value[1] / 1000.0 + keySoundMap[key] = KeySound(startTime: startTime, duration: duration) + } + } + } + } catch {} + } + + func toggleSound() { + isEnabled.toggle() + } + + func playKeyboardSound(forKey key: String = "1") { + guard isEnabled, + let soundData = soundData, + let player = try? AVAudioPlayer(data: soundData) else { + return + } + + let keySound = keySoundMap[key] ?? keySoundMap["1"] + if let soundInfo = keySound { + player.enableRate = true + player.volume = 0.5 + player.currentTime = soundInfo.startTime + player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + soundInfo.duration) { + player.stop() + } + } + } +} diff --git a/freewrite/ContentView.swift b/freewrite/ContentView.swift index cc4113a..31b2764 100644 --- a/freewrite/ContentView.swift +++ b/freewrite/ContentView.swift @@ -82,6 +82,8 @@ struct ContentView: View { @State private var isHoveringHistoryText = false @State private var isHoveringHistoryPath = false @State private var isHoveringHistoryArrow = false + @State private var isHoveringSound = false + @State private var isSoundEnabled = false @State private var colorScheme: ColorScheme = .light // Add state for color scheme @State private var isHoveringThemeToggle = false // Add state for theme toggle hover let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -396,6 +398,18 @@ struct ContentView: View { TextEditor(text: Binding( get: { text }, set: { newValue in + // Play keyboard sound when text changes + if newValue.count != text.count { + // Get the key that was pressed + if let event = NSApp.currentEvent, event.type == .keyDown { + let keyCode = String(event.keyCode) + AudioManager.shared.playKeyboardSound(forKey: keyCode) + } else { + // Fallback to default sound if we can't determine the key + AudioManager.shared.playKeyboardSound() + } + } + // Ensure the text always starts with two newlines if !newValue.hasPrefix("\n\n") { text = "\n\n" + newValue.trimmingCharacters(in: .newlines) @@ -563,6 +577,28 @@ struct ContentView: View { // Utility buttons (moved to right) HStack(spacing: 8) { + Button(action: { + AudioManager.shared.toggleSound() + isSoundEnabled.toggle() // Update state immediately + }) { + Image(systemName: isSoundEnabled ? "speaker.wave.2.fill" : "speaker.slash.fill") + .frame(width: 20, height: 16) // Fixed frame size for both icons + } + .buttonStyle(.plain) + .foregroundColor(isHoveringSound ? .black : .gray) + .onHover { hovering in + isHoveringSound = hovering + isHoveringBottomNav = hovering + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + + Text("•") + .foregroundColor(.gray) + Button(timerButtonTitle) { let now = Date() if let lastClick = lastClickTime, @@ -1305,4 +1341,4 @@ extension NSView { #Preview { ContentView() -} \ No newline at end of file +} diff --git a/freewrite/Sounds/crystal_purple.mp3 b/freewrite/Sounds/crystal_purple.mp3 new file mode 100644 index 0000000..2042a3d Binary files /dev/null and b/freewrite/Sounds/crystal_purple.mp3 differ diff --git a/freewrite/Sounds/crystal_purple_config.json b/freewrite/Sounds/crystal_purple_config.json new file mode 100644 index 0000000..0e5b6d6 --- /dev/null +++ b/freewrite/Sounds/crystal_purple_config.json @@ -0,0 +1,469 @@ +{ + "id": "sound-pack-1200000000009", + "name": "EG Crystal Purple", + "default": true, + "key_define_type": "single", + "includes_numpad": false, + "sound": "purple.ogg", + "defines": { + "1": [ + 1088, + 118 + ], + "2": [ + 1839, + 140 + ], + "3": [ + 2207, + 143 + ], + "4": [ + 2507, + 143 + ], + "5": [ + 2809, + 148 + ], + "6": [ + 3099, + 141 + ], + "7": [ + 3408, + 134 + ], + "8": [ + 3716, + 147 + ], + "9": [ + 4017, + 139 + ], + "10": [ + 4352, + 144 + ], + "11": [ + 4657, + 126 + ], + "12": [ + 4946, + 134 + ], + "13": [ + 5250, + 133 + ], + "14": [ + 5566, + 143 + ], + "15": [ + 6792, + 157 + ], + "16": [ + 7080, + 142 + ], + "17": [ + 7367, + 134 + ], + "18": [ + 7680, + 130 + ], + "19": [ + 7973, + 133 + ], + "20": [ + 8259, + 134 + ], + "21": [ + 8549, + 135 + ], + "22": [ + 8838, + 135 + ], + "23": [ + 9103, + 150 + ], + "24": [ + 9405, + 132 + ], + "25": [ + 9703, + 131 + ], + "26": [ + 10015, + 144 + ], + "27": [ + 10397, + 112 + ], + "28": [ + 15361, + 130 + ], + "29": [ + 21408, + 128 + ], + "30": [ + 12061, + 129 + ], + "31": [ + 12339, + 121 + ], + "32": [ + 12632, + 127 + ], + "33": [ + 12916, + 119 + ], + "34": [ + 13227, + 121 + ], + "35": [ + 13526, + 124 + ], + "36": [ + 13819, + 131 + ], + "37": [ + 14115, + 113 + ], + "38": [ + 14406, + 123 + ], + "39": [ + 14711, + 128 + ], + "40": [ + 15022, + 133 + ], + "41": [ + 1088, + 118 + ], + "42": [ + 16600, + 136 + ], + "43": [ + 10718, + 127 + ], + "44": [ + 16879, + 128 + ], + "45": [ + 17196, + 132 + ], + "46": [ + 17503, + 113 + ], + "47": [ + 17788, + 115 + ], + "48": [ + 18069, + 117 + ], + "49": [ + 18349, + 130 + ], + "50": [ + 18653, + 123 + ], + "51": [ + 18964, + 119 + ], + "52": [ + 19282, + 124 + ], + "53": [ + 19594, + 120 + ], + "54": [ + 19966, + 130 + ], + "55": [ + 10398, + 106 + ], + "56": [ + 21956, + 134 + ], + "57": [ + 22501, + 164 + ], + "58": [ + 11797, + 140 + ], + "59": [ + 1837, + 150 + ], + "60": [ + 2210, + 145 + ], + "61": [ + 2503, + 154 + ], + "62": [ + 2806, + 149 + ], + "63": [ + 3099, + 151 + ], + "64": [ + 3405, + 151 + ], + "65": [ + 3712, + 150 + ], + "66": [ + 4015, + 146 + ], + "67": [ + 4354, + 140 + ], + "68": [ + 4650, + 143 + ], + "69": [ + 9700, + 127 + ], + "70": [ + 2205, + 151 + ], + "71": [ + 11070, + 112 + ], + "72": [ + 12338, + 115 + ], + "73": [ + 12634, + 120 + ], + "74": [ + 10718, + 123 + ], + "75": [ + 12912, + 115 + ], + "76": [ + 13226, + 119 + ], + "77": [ + 13529, + 117 + ], + "78": [ + 5567, + 141 + ], + "79": [ + 13814, + 141 + ], + "80": [ + 14114, + 117 + ], + "81": [ + 14401, + 127 + ], + "82": [ + 16592, + 164 + ], + "83": [ + 14715, + 123 + ], + "87": [ + 4941, + 142 + ], + "88": [ + 5249, + 149 + ], + "3612": [ + 11798, + 129 + ], + "3613": [ + 23720, + 119 + ], + "3637": [ + 10015, + 139 + ], + "3639": [ + 1837, + 151 + ], + "3640": [ + 23148, + 122 + ], + "3653": [ + 2499, + 153 + ], + "3655": [ + 7679, + 127 + ], + "3657": [ + 7975, + 123 + ], + "3663": [ + 9110, + 133 + ], + "3665": [ + 9405, + 125 + ], + "3666": [ + 7369, + 119 + ], + "3667": [ + 8839, + 134 + ], + "3675": [ + 21677, + 127 + ], + "3676": [ + 23425, + 120 + ], + "3677": [ + 23425, + 120 + ], + "57416": [ + 20381, + 137 + ], + "57419": [ + 23982, + 128 + ], + "57421": [ + 24215, + 143 + ], + "57424": [ + 24223, + 134 + ], + "60999": [ + 7679, + 127 + ], + "61000": [ + 20381, + 137 + ], + "61001": [ + 7975, + 123 + ], + "61003": [ + 23982, + 128 + ], + "61005": [ + 24215, + 143 + ], + "61007": [ + 9110, + 133 + ], + "61008": [ + 24223, + 134 + ], + "61009": [ + 9405, + 125 + ], + "61010": [ + 7369, + 119 + ], + "61011": [ + 8839, + 134 + ] + }, + "tags": [ + "pre-installed" + ] +}