Skip to content
Open
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
177 changes: 169 additions & 8 deletions freewrite/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ struct ContentView: View {
@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 fadeAfterSeconds: Int? = nil
@State private var secondsSinceTyping = 0
@State private var idleFadeOverlayOpacity: Double = 0.0
@State private var isIdleFading = false
@State private var idleFadeGeneration = UUID()
@State private var isHoveringIdleFade = false
@State private var isHoveringFullscreen = false
@State private var hoveredFont: String? = nil
@State private var isHoveringSize = false
Expand Down Expand Up @@ -143,6 +149,8 @@ struct ContentView: View {
let availableFonts = NSFontManager.shared.availableFontFamilies
let standardFonts = ["Lato-Regular", "Arial", ".AppleSystemUIFont", "Times New Roman"]
let fontSizes: [CGFloat] = [16, 18, 20, 22, 24, 26]
let idleFadeOptions: [Int?] = [nil, 3, 7, 15, 30, 45]
let idleFadeDuration: Double = 7.0
let placeholderOptions = [
"Begin writing",
"Pick a thought and go",
Expand Down Expand Up @@ -841,6 +849,121 @@ struct ContentView: View {
return isHoveringTimer ? (colorScheme == .light ? .black : .white) : (colorScheme == .light ? .gray : .gray.opacity(0.8))
}
}

var idleFadeButtonTitle: String {
guard let fadeAfterSeconds else {
return "Don't fade"
}
return "Fade in \(fadeAfterSeconds)s"
}

var idleFadeColor: Color {
if fadeAfterSeconds != nil {
return isHoveringIdleFade ? (colorScheme == .light ? .black : .white) : .gray.opacity(0.8)
} else {
return isHoveringIdleFade ? (colorScheme == .light ? .black : .white) : (colorScheme == .light ? .gray : .gray.opacity(0.8))
}
}

private func setIdleFadeSeconds(_ seconds: Int?) {
fadeAfterSeconds = seconds
resetIdleFadeProgress()
}

private func adjustIdleFadeSeconds(by direction: Int) {
guard let currentIndex = idleFadeOptions.firstIndex(where: { $0 == fadeAfterSeconds }) else {
setIdleFadeSeconds(7)
return
}

let nextIndex = min(max(currentIndex + direction, 0), idleFadeOptions.count - 1)
setIdleFadeSeconds(idleFadeOptions[nextIndex])
}

private func resetIdleFadeProgress() {
secondsSinceTyping = 0
idleFadeGeneration = UUID()

if isIdleFading || idleFadeOverlayOpacity > 0 {
withAnimation(.easeOut(duration: 0.2)) {
idleFadeOverlayOpacity = 0
}
isIdleFading = false
}
}

private func updateIdleFadeProgress() {
guard timerIsRunning,
fadeAfterSeconds != nil,
currentVideoURL == nil,
!isIdleFading else {
return
}

secondsSinceTyping += 1
if let fadeAfterSeconds, secondsSinceTyping >= fadeAfterSeconds {
startIdleFade()
}
}

private func startIdleFade() {
guard timerIsRunning,
fadeAfterSeconds != nil,
currentVideoURL == nil,
!isIdleFading else {
return
}

let fadeGeneration = UUID()
idleFadeGeneration = fadeGeneration
isIdleFading = true

withAnimation(.linear(duration: idleFadeDuration)) {
idleFadeOverlayOpacity = 1.0
}

DispatchQueue.main.asyncAfter(deadline: .now() + idleFadeDuration) {
guard idleFadeGeneration == fadeGeneration, isIdleFading else {
return
}

finishCurrentWritingSession(markTimerComplete: true)
withAnimation(.easeInOut(duration: 0.2)) {
showingSidebar = true
}
timeRemaining = 900
createNewEntry()
secondsSinceTyping = 0

withAnimation(.easeOut(duration: 1.0)) {
idleFadeOverlayOpacity = 0
}

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
if idleFadeGeneration == fadeGeneration {
isIdleFading = false
}
}
}
}

private func finishCurrentWritingSession(markTimerComplete: Bool = false) {
timerIsRunning = false

if markTimerComplete {
timeRemaining = 0
}

if let currentId = selectedEntryId,
let currentEntry = entries.first(where: { $0.id == currentId }),
currentEntry.entryType == .text {
saveEntry(entry: currentEntry)
}

withAnimation(.easeOut(duration: 1.0)) {
bottomNavOpacity = 1.0
}
}

var lineHeight: CGFloat {
let font = NSFont(name: selectedFont, size: fontSize) ?? .systemFont(ofSize: fontSize)
Expand Down Expand Up @@ -1090,10 +1213,11 @@ struct ContentView: View {
if let lastClick = lastClickTime,
now.timeIntervalSince(lastClick) < 0.3 {
timeRemaining = 900
timerIsRunning = false
finishCurrentWritingSession()
lastClickTime = nil
} else {
timerIsRunning.toggle()
resetIdleFadeProgress()
lastClickTime = now
}
}
Expand Down Expand Up @@ -1127,6 +1251,38 @@ struct ContentView: View {
}
}

Text("•")
.foregroundColor(.gray)

Button(idleFadeButtonTitle) {
setIdleFadeSeconds(fadeAfterSeconds == nil ? 7 : nil)
}
.buttonStyle(.plain)
.foregroundColor(idleFadeColor)
.onHover { hovering in
isHoveringIdleFade = hovering
isHoveringBottomNav = hovering
if hovering {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
.onAppear {
NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in
if isHoveringIdleFade {
let scrollBuffer = event.deltaY * 0.25

if abs(scrollBuffer) >= 0.1 {
NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .now)
let direction = -scrollBuffer > 0 ? 1 : -1
adjustIdleFadeSeconds(by: direction)
}
}
return event
}
}

Text("•")
.foregroundColor(.gray)

Expand Down Expand Up @@ -1502,6 +1658,11 @@ struct ContentView: View {
}
}
}

Color(colorScheme == .light ? .white : .black)
.opacity(idleFadeOverlayOpacity)
.ignoresSafeArea()
.allowsHitTesting(false)
}

// Right sidebar
Expand Down Expand Up @@ -1716,7 +1877,9 @@ struct ContentView: View {
clearVideoRecordingPreparationState()
}
}
.onChange(of: text) { _ in
.onChange(of: text) { _, _ in
resetIdleFadeProgress()

// Save current entry when text changes
if let currentId = selectedEntryId,
let currentEntry = entries.first(where: { $0.id == currentId }),
Expand All @@ -1727,13 +1890,11 @@ struct ContentView: View {
.onReceive(timer) { _ in
if timerIsRunning && timeRemaining > 0 {
timeRemaining -= 1
} else if timeRemaining == 0 {
timerIsRunning = false
if !isHoveringBottomNav {
withAnimation(.easeOut(duration: 1.0)) {
bottomNavOpacity = 1.0
}
if timeRemaining > 0 {
updateIdleFadeProgress()
}
} else if timerIsRunning && timeRemaining == 0 {
finishCurrentWritingSession()
}
}
.onReceive(NotificationCenter.default.publisher(for: NSWindow.willEnterFullScreenNotification)) { _ in
Expand Down