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
6 changes: 1 addition & 5 deletions Crisp/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
})
let toolshead = block("toolshead") {
VStack(alignment: .leading, spacing: 0) {
SectionDivider()
ExpandableRowStateful(icon: "wrench.and.screwdriver.fill", iconActive: false,
label: "Tools", state: state, key: \.showTools)
}
ToolsHeaderBlock(state: state)
}
toolshead.liveInFlight = true
blocks.append(toolshead)
Expand Down
77 changes: 77 additions & 0 deletions Crisp/Models/KeepAwakeDuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation

enum KeepAwakeDuration: Int, CaseIterable, Identifiable {
case indefinite = 0
case minutes15 = 900
case minutes30 = 1800
case hour1 = 3600
case hours2 = 7200

var id: Int { rawValue }

/// Duration in seconds, or nil when indefinite (`Always`).
var seconds: TimeInterval? {
self == .indefinite ? nil : TimeInterval(rawValue)
}

/// Compact label rendered inside each duration pill.
var pillLabel: String {
switch self {
case .indefinite: return String(localized: "Always")
case .minutes15: return String(localized: "15m")
case .minutes30: return String(localized: "30m")
case .hour1: return String(localized: "1h")
case .hours2: return String(localized: "2h")
}
}

/// Spoken VoiceOver label for each duration pill.
var accessibilityLabel: String {
switch self {
case .indefinite: return String(localized: "Always")
case .minutes15: return String(localized: "15 Minutes")
case .minutes30: return String(localized: "30 Minutes")
case .hour1: return String(localized: "1 Hour")
case .hours2: return String(localized: "2 Hours")
}
}

/// Rounds positive remaining seconds up to whole minutes (`1...`), or `0` when expired (`<= 0`).
static func remainingMinutes(for remainingSeconds: TimeInterval) -> Int {
guard remainingSeconds > 0 else { return 0 }
return max(1, Int(ceil(remainingSeconds / 60.0)))
}

/// Formats a positive minute count as a localized time span (`"42m"`, `"1h"`, `"1h 30m"`).
/// Uses only single-argument `String(localized:)` keys (`"%lldh"`, `"%lldm"`) so `xcstrings-compile.py`
/// and `xcodebuild -exportLocalizations` produce identical non-positional catalog keys.
static func formattedSpan(minutes: Int) -> String {
let clamped = max(1, minutes)
let hours = clamped / 60
let remMins = clamped % 60
if hours > 0 {
let hPart = String(localized: "\(hours)h")
guard remMins > 0 else { return hPart }
let mPart = String(localized: "\(remMins)m")
return "\(hPart) \(mPart)"
}
return String(localized: "\(clamped)m")
}

/// Formats the inline countdown subtitle (`"42m left"`, `"1h 30m left"`).
static func countdownLabel(minutes: Int) -> String {
let span = formattedSpan(minutes: minutes)
return String(localized: "\(span) left")
}

/// Formats the collapsed `Tools` row subtitle:
/// - `nil` when `isActive == false`
/// - `"Keep Awake"` when active indefinitely (`remainingMinutes == nil`)
/// - `"Keep Awake · 42m left"` when timed (`remainingMinutes != nil`)
static func toolsSubtitle(isActive: Bool, remainingMinutes: Int?) -> String? {
guard isActive else { return nil }
let title = String(localized: "Keep Awake")
guard let mins = remainingMinutes, mins > 0 else { return title }
return "\(title) · \(countdownLabel(minutes: mins))"
}
}
192 changes: 192 additions & 0 deletions Crisp/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,22 @@
}
}
},
"%@ left" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "剩余 %@"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "剩餘 %@"
}
}
}
},
"%@ off" : {
"localizations" : {
"zh-Hans" : {
Expand Down Expand Up @@ -237,6 +253,166 @@
"%lld%%" : {
"shouldTranslate" : false
},
"%lldh" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 小时"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 小時"
}
}
}
},
"%lldm" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 分钟"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 分鐘"
}
}
}
},
"1 Hour" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 小时"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 小時"
}
}
}
},
"1h" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "1小时"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "1小時"
}
}
}
},
"2 Hours" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "2 小时"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "2 小時"
}
}
}
},
"2h" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "2小时"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "2小時"
}
}
}
},
"15 Minutes" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "15 分钟"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "15 分鐘"
}
}
}
},
"15m" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "15分"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "15分"
}
}
}
},
"30 Minutes" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "30 分钟"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "30 分鐘"
}
}
}
},
"30m" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "30分"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "30分"
}
}
}
},
"Another input disconnects this display. Reconnect it here to switch back." : {
"localizations" : {
"zh-Hans" : {
Expand Down Expand Up @@ -644,6 +820,22 @@
}
}
},
"Always" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "始终"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "始終"
}
}
}
},
"Applies a preset saved in Crisp, the same as clicking it in the menu." : {
"localizations" : {
"zh-Hans" : {
Expand Down
Loading