Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
609 changes: 609 additions & 0 deletions freewrite.xcodeproj/project.pbxproj.backup

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions freewrite/AudioManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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)
} else {
print("Failed to load crystal_purple.mp3")
}
}

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)
} else {
print("Failed to load crystal_purple_config.json")
}
}

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)
}
}
print("Successfully loaded \(keySoundMap.count) key sounds")
} else {
print("Failed to parse defines from config")
}
} catch {
print("Error loading config: \(error)")
}
}

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()
}
}
}
}
99 changes: 95 additions & 4 deletions freewrite/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import SwiftUI
import AppKit
import AVFoundation
import UniformTypeIdentifiers
import PDFKit

Expand Down Expand Up @@ -49,15 +50,15 @@ struct ContentView: View {
@State private var text: String = "" // Remove initial welcome text since we'll handle it in createNewEntry

@State private var isFullscreen = false
@State private var selectedFont: String = "Lato-Regular"
@State private var selectedFont: String = UserDefaults.standard.string(forKey: "selectedFont") ?? "Lato-Regular"
@State private var currentRandomFont: String = ""
@State private var timeRemaining: Int = 900 // Changed to 900 seconds (15 minutes)
@State private var timerIsRunning = false
@State private var isHoveringTimer = false
@State private var isHoveringFullscreen = false
@State private var hoveredFont: String? = nil
@State private var isHoveringSize = false
@State private var fontSize: CGFloat = 18
@State private var fontSize: CGFloat = UserDefaults.standard.float(forKey: "fontSize") > 0 ? CGFloat(UserDefaults.standard.float(forKey: "fontSize")) : 18
@State private var blinkCount = 0
@State private var isBlinking = false
@State private var opacity: Double = 1.0
Expand All @@ -82,6 +83,11 @@ struct ContentView: View {
@State private var isHoveringHistoryText = false
@State private var isHoveringHistoryPath = false
@State private var isHoveringHistoryArrow = false
@State private var showWordCount = false
@State private var wordCount = 0
@State private var characterCount = 0
@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()
Expand Down Expand Up @@ -396,6 +402,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)
Expand Down Expand Up @@ -442,6 +460,7 @@ struct ContentView: View {
if let currentIndex = fontSizes.firstIndex(of: fontSize) {
let nextIndex = (currentIndex + 1) % fontSizes.count
fontSize = fontSizes[nextIndex]
UserDefaults.standard.set(Float(fontSize), forKey: "fontSize")
}
}
.buttonStyle(.plain)
Expand All @@ -462,6 +481,7 @@ struct ContentView: View {
Button("Lato") {
selectedFont = "Lato-Regular"
currentRandomFont = ""
UserDefaults.standard.set("Lato-Regular", forKey: "selectedFont")
}
.buttonStyle(.plain)
.foregroundColor(hoveredFont == "Lato" ? textHoverColor : textColor)
Expand All @@ -481,6 +501,7 @@ struct ContentView: View {
Button("Arial") {
selectedFont = "Arial"
currentRandomFont = ""
UserDefaults.standard.set("Arial", forKey: "selectedFont")
}
.buttonStyle(.plain)
.foregroundColor(hoveredFont == "Arial" ? textHoverColor : textColor)
Expand All @@ -500,6 +521,7 @@ struct ContentView: View {
Button("System") {
selectedFont = ".AppleSystemUIFont"
currentRandomFont = ""
UserDefaults.standard.set(".AppleSystemUIFont", forKey: "selectedFont")
}
.buttonStyle(.plain)
.foregroundColor(hoveredFont == "System" ? textHoverColor : textColor)
Expand All @@ -519,6 +541,7 @@ struct ContentView: View {
Button("Serif") {
selectedFont = "Times New Roman"
currentRandomFont = ""
UserDefaults.standard.set("Times New Roman", forKey: "selectedFont")
}
.buttonStyle(.plain)
.foregroundColor(hoveredFont == "Serif" ? textHoverColor : textColor)
Expand All @@ -539,6 +562,7 @@ struct ContentView: View {
if let randomFont = availableFonts.randomElement() {
selectedFont = randomFont
currentRandomFont = randomFont
UserDefaults.standard.set(randomFont, forKey: "selectedFont")
}
}
.buttonStyle(.plain)
Expand All @@ -563,6 +587,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,
Expand All @@ -572,6 +618,16 @@ struct ContentView: View {
lastClickTime = nil
} else {
timerIsRunning.toggle()
// Hide word count when starting a new timer session
if timerIsRunning {
showWordCount = false
// Ensure bottom nav fades out when starting a new session
if !isHoveringBottomNav {
withAnimation(.easeIn(duration: 1.0)) {
bottomNavOpacity = 0.0
}
}
}
lastClickTime = now
}
}
Expand Down Expand Up @@ -701,14 +757,29 @@ struct ContentView: View {
Text("•")
.foregroundColor(.gray)

// Word and character count display
if showWordCount {
Text("\(wordCount) words | \(characterCount) chars")
.font(.system(size: 13))
.foregroundColor(.gray)
.padding(.horizontal, 4)
}

Text("•")
.foregroundColor(.gray)
.opacity(showWordCount ? 1.0 : 0.0)

Button(action: {
// Hide word count when creating a new entry
showWordCount = false
createNewEntry()
}) {
Text("New Entry")
.font(.system(size: 13))
}
.buttonStyle(.plain)
.foregroundColor(isHoveringNewEntry ? textHoverColor : textColor)
.foregroundColor(timerIsRunning ? Color.gray.opacity(0.5) : (isHoveringNewEntry ? textHoverColor : textColor))
.disabled(timerIsRunning) // Disable the button while timer is running
.onHover { hovering in
isHoveringNewEntry = hovering
isHoveringBottomNav = hovering
Expand Down Expand Up @@ -956,6 +1027,11 @@ struct ContentView: View {
timeRemaining -= 1
} else if timeRemaining == 0 {
timerIsRunning = false
// Calculate word and character count when timer ends
calculateWordAndCharacterCount()
showWordCount = true
// Reset timeRemaining for the next session
timeRemaining = 900
if !isHoveringBottomNav {
withAnimation(.easeOut(duration: 1.0)) {
bottomNavOpacity = 1.0
Expand Down Expand Up @@ -1029,6 +1105,9 @@ struct ContentView: View {
}

private func createNewEntry() {
// Hide word count when creating a new entry
showWordCount = false

let newEntry = HumanEntry.createNew()
entries.insert(newEntry, at: 0) // Add to the beginning
selectedEntryId = newEntry.id
Expand Down Expand Up @@ -1074,6 +1153,18 @@ struct ContentView: View {
}
}

// Calculate word and character count from the current text
private func calculateWordAndCharacterCount() {
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)

// Calculate character count (excluding whitespace)
characterCount = trimmedText.count

// Calculate word count
let components = trimmedText.components(separatedBy: .whitespacesAndNewlines)
wordCount = components.filter { !$0.isEmpty }.count
}

private func deleteEntry(entry: HumanEntry) {
// Delete the file from the filesystem
let documentsDirectory = getDocumentsDirectory()
Expand Down Expand Up @@ -1305,4 +1396,4 @@ extension NSView {

#Preview {
ContentView()
}
}
74 changes: 74 additions & 0 deletions freewrite/CustomTextEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import SwiftUI
import AppKit

struct CustomTextEditor: NSViewRepresentable {
@Binding var text: String
var font: NSFont
var textColor: NSColor
var backgroundColor: NSColor

class Coordinator: NSObject, NSTextViewDelegate {
var parent: CustomTextEditor

init(_ parent: CustomTextEditor) {
self.parent = parent
}

func textDidChange(_ notification: Notification) {
if let textView = notification.object as? NSTextView {
parent.text = textView.string
}
}
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

func makeNSView(context: Context) -> NSScrollView {
let textView = CustomNSTextView()
textView.delegate = context.coordinator
textView.font = font
textView.textColor = textColor
textView.backgroundColor = backgroundColor
textView.string = text
textView.isRichText = false
textView.isEditable = true
textView.isSelectable = true
textView.allowsUndo = true
textView.drawsBackground = true
textView.autoresizingMask = [.width, .height]
textView.minSize = NSSize(width: 0, height: 0)
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.textContainer?.widthTracksTextView = true
textView.textContainerInset = NSSize(width: 8, height: 8)

let scrollView = NSScrollView()
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autohidesScrollers = true
scrollView.backgroundColor = backgroundColor
return scrollView
}

func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textView = nsView.documentView as? NSTextView {
if textView.string != text {
textView.string = text
}
textView.font = font
textView.textColor = textColor
textView.backgroundColor = backgroundColor
}
}
}

class CustomNSTextView: NSTextView {
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
if let chars = event.characters, !chars.isEmpty {
AudioManager.shared.playKeyboardSound(forKey: String(chars.first!))
}
}
}
Binary file added freewrite/Sounds/crystal_purple.mp3
Binary file not shown.
Loading