Compare commits
3 Commits
3b45fe2114
...
4190c95b84
| Author | SHA1 | Date | |
|---|---|---|---|
| 4190c95b84 | |||
| 4f46304a5b | |||
| 089f8b9f7b |
3
PRD.md
3
PRD.md
@ -798,6 +798,9 @@ Use **iPhone 17 Pro Max (iOS 26.2)** as the primary simulator for build and test
|
||||
- **Async/Await**: Modern concurrency patterns throughout the codebase
|
||||
- **Observation Framework**: @Observable for reactive state management
|
||||
- **SwiftUI Navigation**: Latest NavigationStack and navigation APIs with iOS 26 features
|
||||
- **Localization**: User-facing runtime/accessibility strings use modern `String(localized:)` with `Localizable.xcstrings` coverage for English (`en`), Spanish - Mexico (`es-MX`), and French - Canada (`fr-CA`)
|
||||
- **Settings Localization**: Clock settings screens use explicit localization keys for all section labels, control titles/subtitles, and helper copy
|
||||
- **Localization Consistency**: App localization catalog uses a key-only convention (namespaced keys) to avoid mixed source-string and keyed entries
|
||||
- **Accessibility**: Full VoiceOver and Dynamic Type support with iOS 26 enhancements
|
||||
- **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass
|
||||
- **Performance**: Optimized for 120Hz ProMotion displays and iOS 26 performance improvements
|
||||
|
||||
@ -146,6 +146,9 @@ Swift access is provided via:
|
||||
- Alarm list ordering now prioritizes enabled alarms and sorts by the next trigger time for faster at-a-glance relevance.
|
||||
- Repeat-day controls now follow locale weekday order and include accessibility identifiers for repeat/alert options controls.
|
||||
- Onboarding now explicitly highlights repeat schedules plus per-alarm vibration/flash/volume customization for better feature discovery.
|
||||
- Non-`Text` user-facing strings were migrated to `String(localized:)` and backed by `TheNoiseClock/Localizable.xcstrings` with `en`, `es-MX`, and `fr-CA` translations.
|
||||
- Clock Settings UI copy now uses explicit localization keys (no raw `Text(\"...\")` literals in settings screens).
|
||||
- Localization catalog is normalized to a single pattern: namespaced key-based entries only (no source-string keys).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -79,25 +79,25 @@ struct ContentView: View {
|
||||
|
||||
private var mainTabView: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
Tab("Clock", systemImage: "clock", value: AppTab.clock) {
|
||||
Tab(String(localized: "tab.clock", defaultValue: "Clock"), systemImage: "clock", value: AppTab.clock) {
|
||||
NavigationStack {
|
||||
ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab)
|
||||
}
|
||||
}
|
||||
|
||||
Tab("Alarms", systemImage: "alarm", value: AppTab.alarms) {
|
||||
Tab(String(localized: "tab.alarms", defaultValue: "Alarms"), systemImage: "alarm", value: AppTab.alarms) {
|
||||
NavigationStack {
|
||||
AlarmView(viewModel: alarmViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Tab("Noise", systemImage: "waveform", value: AppTab.noise) {
|
||||
Tab(String(localized: "tab.noise", defaultValue: "Noise"), systemImage: "waveform", value: AppTab.noise) {
|
||||
NavigationStack {
|
||||
NoiseView()
|
||||
}
|
||||
}
|
||||
|
||||
Tab("Settings", systemImage: "gearshape", value: AppTab.settings) {
|
||||
Tab(String(localized: "tab.settings", defaultValue: "Settings"), systemImage: "gearshape", value: AppTab.settings) {
|
||||
NavigationStack {
|
||||
ClockSettingsView(
|
||||
style: clockViewModel.style,
|
||||
|
||||
@ -17,10 +17,15 @@ import Foundation
|
||||
/// Intent to stop an active alarm from the Live Activity or notification.
|
||||
struct StopAlarmIntent: LiveActivityIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Stop Alarm"
|
||||
static let description = IntentDescription("Stops the currently ringing alarm")
|
||||
static let title = LocalizedStringResource("alarm_intent.stop.title", defaultValue: "Stop Alarm")
|
||||
static let description = IntentDescription(
|
||||
LocalizedStringResource(
|
||||
"alarm_intent.stop.description",
|
||||
defaultValue: "Stops the currently ringing alarm"
|
||||
)
|
||||
)
|
||||
|
||||
@Parameter(title: "Alarm ID")
|
||||
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
|
||||
var alarmId: String
|
||||
|
||||
static var supportedModes: IntentModes { .background }
|
||||
@ -48,10 +53,15 @@ struct StopAlarmIntent: LiveActivityIntent {
|
||||
/// Intent to snooze an active alarm from the Live Activity or notification.
|
||||
struct SnoozeAlarmIntent: LiveActivityIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Snooze Alarm"
|
||||
static let description = IntentDescription("Snoozes the currently ringing alarm")
|
||||
static let title = LocalizedStringResource("alarm_intent.snooze.title", defaultValue: "Snooze Alarm")
|
||||
static let description = IntentDescription(
|
||||
LocalizedStringResource(
|
||||
"alarm_intent.snooze.description",
|
||||
defaultValue: "Snoozes the currently ringing alarm"
|
||||
)
|
||||
)
|
||||
|
||||
@Parameter(title: "Alarm ID")
|
||||
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
|
||||
var alarmId: String
|
||||
|
||||
static var supportedModes: IntentModes { .background }
|
||||
@ -80,11 +90,16 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
|
||||
/// Intent to open the app when the user taps the Live Activity.
|
||||
struct OpenAlarmAppIntent: LiveActivityIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Open TheNoiseClock"
|
||||
static let description = IntentDescription("Opens the app to the alarm screen")
|
||||
static let title = LocalizedStringResource("alarm_intent.open_app.title", defaultValue: "Open TheNoiseClock")
|
||||
static let description = IntentDescription(
|
||||
LocalizedStringResource(
|
||||
"alarm_intent.open_app.description",
|
||||
defaultValue: "Opens the app to the alarm screen"
|
||||
)
|
||||
)
|
||||
static let openAppWhenRun = true
|
||||
|
||||
@Parameter(title: "Alarm ID")
|
||||
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
|
||||
var alarmId: String
|
||||
|
||||
init() {
|
||||
@ -110,9 +125,9 @@ enum AlarmIntentError: Error, LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidAlarmID:
|
||||
return "Invalid alarm ID"
|
||||
return String(localized: "alarm_intent.error.invalid_alarm_id", defaultValue: "Invalid alarm ID")
|
||||
case .alarmNotFound:
|
||||
return "Alarm not found"
|
||||
return String(localized: "alarm_intent.error.alarm_not_found", defaultValue: "Alarm not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,8 +43,8 @@ struct Alarm: Identifiable, Codable, Equatable {
|
||||
isEnabled: Bool = true,
|
||||
repeatWeekdays: [Int] = [],
|
||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||
label: String = "Alarm",
|
||||
notificationMessage: String = "Your alarm is ringing",
|
||||
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
|
||||
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
|
||||
snoozeDuration: Int = 9,
|
||||
isVibrationEnabled: Bool = true,
|
||||
isLightFlashEnabled: Bool = false,
|
||||
@ -72,8 +72,8 @@ struct Alarm: Identifiable, Codable, Equatable {
|
||||
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
|
||||
)
|
||||
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
|
||||
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm"
|
||||
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? "Your alarm is ringing"
|
||||
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? String(localized: "alarm.default_label", defaultValue: "Alarm")
|
||||
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
|
||||
self.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9
|
||||
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
|
||||
self.isLightFlashEnabled = try container.decodeIfPresent(Bool.self, forKey: .isLightFlashEnabled) ?? false
|
||||
@ -127,19 +127,21 @@ struct Alarm: Identifiable, Codable, Equatable {
|
||||
|
||||
static func repeatSummary(for weekdays: [Int]) -> String {
|
||||
let normalized = sanitizedWeekdays(weekdays)
|
||||
guard !normalized.isEmpty else { return "Once" }
|
||||
guard !normalized.isEmpty else {
|
||||
return String(localized: "alarm.repeat.once", defaultValue: "Once")
|
||||
}
|
||||
|
||||
let weekdaySet = Set(normalized)
|
||||
if weekdaySet.count == 7 {
|
||||
return "Every day"
|
||||
return String(localized: "alarm.repeat.every_day", defaultValue: "Every day")
|
||||
}
|
||||
|
||||
if weekdaySet == Set([2, 3, 4, 5, 6]) {
|
||||
return "Weekdays"
|
||||
return String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays")
|
||||
}
|
||||
|
||||
if weekdaySet == Set([1, 7]) {
|
||||
return "Weekends"
|
||||
return String(localized: "alarm.repeat.weekends", defaultValue: "Weekends")
|
||||
}
|
||||
|
||||
let symbols = Calendar.current.shortWeekdaySymbols
|
||||
|
||||
@ -83,14 +83,14 @@ final class AlarmKitService {
|
||||
|
||||
// Create the stop button for the alarm
|
||||
let stopButton = AlarmButton(
|
||||
text: "Stop",
|
||||
text: LocalizedStringResource("alarm.action.stop"),
|
||||
textColor: .red,
|
||||
systemImageName: "stop.fill"
|
||||
)
|
||||
|
||||
// Create the snooze button (secondary button with countdown behavior)
|
||||
let snoozeButton = AlarmButton(
|
||||
text: "Snooze",
|
||||
text: LocalizedStringResource("alarm.action.snooze"),
|
||||
textColor: .white,
|
||||
systemImageName: "moon.zzz"
|
||||
)
|
||||
@ -422,9 +422,15 @@ enum AlarmKitError: Error, LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notAuthorized:
|
||||
return "AlarmKit is not authorized. Please enable alarm permissions in Settings."
|
||||
return String(
|
||||
localized: "alarmkit.error.not_authorized",
|
||||
defaultValue: "AlarmKit is not authorized. Please enable alarm permissions in Settings."
|
||||
)
|
||||
case .schedulingFailed(let error):
|
||||
return "Failed to schedule alarm: \(error.localizedDescription)"
|
||||
return String(
|
||||
localized: "alarmkit.error.scheduling_failed",
|
||||
defaultValue: "Failed to schedule alarm: \(error.localizedDescription)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,8 +152,8 @@ final class AlarmViewModel {
|
||||
time: Date,
|
||||
repeatWeekdays: [Int] = [],
|
||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||
label: String = "Alarm",
|
||||
notificationMessage: String = "Your alarm is ringing",
|
||||
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
|
||||
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
|
||||
snoozeDuration: Int = 9,
|
||||
isVibrationEnabled: Bool = true,
|
||||
isLightFlashEnabled: Bool = false,
|
||||
@ -219,6 +219,9 @@ final class AlarmViewModel {
|
||||
} else {
|
||||
error.localizedDescription
|
||||
}
|
||||
return "Unable to \(action) alarm. \(detail)"
|
||||
return String(
|
||||
localized: "alarm.operation.error_message",
|
||||
defaultValue: "Unable to \(action) alarm. \(detail)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,8 @@ struct AddAlarmView: View {
|
||||
@State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date()
|
||||
@State private var repeatWeekdays: [Int] = []
|
||||
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
|
||||
@State private var alarmLabel = "Alarm"
|
||||
@State private var notificationMessage = "Your alarm is ringing"
|
||||
@State private var alarmLabel = String(localized: "alarm.default_label", defaultValue: "Alarm")
|
||||
@State private var notificationMessage = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
|
||||
@State private var snoozeDuration = 9 // minutes
|
||||
@State private var isVibrationEnabled = true
|
||||
@State private var isLightFlashEnabled = false
|
||||
@ -44,7 +44,7 @@ struct AddAlarmView: View {
|
||||
Image(systemName: "textformat")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Label")
|
||||
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
|
||||
Spacer()
|
||||
Text(alarmLabel)
|
||||
.foregroundStyle(.secondary)
|
||||
@ -57,7 +57,7 @@ struct AddAlarmView: View {
|
||||
Image(systemName: "message")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Message")
|
||||
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
|
||||
Spacer()
|
||||
Text(notificationMessage)
|
||||
.foregroundStyle(.secondary)
|
||||
@ -71,7 +71,7 @@ struct AddAlarmView: View {
|
||||
Image(systemName: "music.note")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Sound")
|
||||
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
|
||||
Spacer()
|
||||
Text(getSoundDisplayName(selectedSoundName))
|
||||
.foregroundStyle(.secondary)
|
||||
@ -84,7 +84,7 @@ struct AddAlarmView: View {
|
||||
Image(systemName: "repeat")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Repeat")
|
||||
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
|
||||
Spacer()
|
||||
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||
.foregroundStyle(.secondary)
|
||||
@ -98,9 +98,14 @@ struct AddAlarmView: View {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Snooze")
|
||||
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
|
||||
Spacer()
|
||||
Text("for \(snoozeDuration) min")
|
||||
Text(
|
||||
String(
|
||||
localized: "alarm.editor.snooze_for_minutes",
|
||||
defaultValue: "for \(snoozeDuration) min"
|
||||
)
|
||||
)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@ -113,12 +118,12 @@ struct AddAlarmView: View {
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.navigationTitle("Alarm")
|
||||
.navigationTitle(String(localized: "alarm.add.navigation_title", defaultValue: "Alarm"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||
isPresented = false
|
||||
}
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
@ -126,7 +131,11 @@ struct AddAlarmView: View {
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(isSaving ? "Saving..." : "Save") {
|
||||
Button(
|
||||
isSaving
|
||||
? String(localized: "common.saving", defaultValue: "Saving...")
|
||||
: String(localized: "common.save", defaultValue: "Save")
|
||||
) {
|
||||
Task {
|
||||
await saveAlarm()
|
||||
}
|
||||
@ -138,8 +147,8 @@ struct AddAlarmView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
|
||||
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
|
||||
} message: {
|
||||
Text(saveErrorMessage)
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ struct AlarmView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.navigationTitle(isPad ? "" : "Alarms")
|
||||
.navigationTitle(isPad ? "" : String(localized: "alarms.navigation_title", defaultValue: "Alarms"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@ -107,8 +107,8 @@ struct AlarmView: View {
|
||||
}
|
||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
|
||||
.accessibilityIdentifier("alarms.screen")
|
||||
.alert("Alarm Error", isPresented: $viewModel.isShowingErrorAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $viewModel.isShowingErrorAlert) {
|
||||
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {
|
||||
viewModel.dismissErrorAlert()
|
||||
}
|
||||
} message: {
|
||||
|
||||
@ -36,7 +36,12 @@ struct AlarmRowView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
|
||||
Text("• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
|
||||
Text(
|
||||
String(
|
||||
localized: "alarm.row.sound_prefix",
|
||||
defaultValue: "• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
|
||||
@ -46,7 +51,7 @@ struct AlarmRowView: View {
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AppStatus.warning)
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
Text("Foreground only for full alarm sound")
|
||||
Text(String(localized: "alarm.row.keep_awake_warning", defaultValue: "Foreground only for full alarm sound"))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
@ -73,14 +78,23 @@ struct AlarmRowView: View {
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
|
||||
.accessibilityLabel("\(alarm.label), \(alarm.formattedTime())")
|
||||
.accessibilityValue(alarm.isEnabled ? "Enabled" : "Disabled")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
localized: "alarm.row.accessibility_label",
|
||||
defaultValue: "\(alarm.label), \(alarm.formattedTime())"
|
||||
)
|
||||
)
|
||||
.accessibilityValue(
|
||||
alarm.isEnabled
|
||||
? String(localized: "common.enabled", defaultValue: "Enabled")
|
||||
: String(localized: "common.disabled", defaultValue: "Disabled")
|
||||
)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
onDelete()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
Label(String(localized: "common.delete", defaultValue: "Delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,20 +15,20 @@ struct AlertOptionsSection: View {
|
||||
@Binding var volume: Float
|
||||
|
||||
var body: some View {
|
||||
Section("Alert Options") {
|
||||
Toggle("Vibration", isOn: $isVibrationEnabled)
|
||||
Section(String(localized: "alarm.alert_options.section_title", defaultValue: "Alert Options")) {
|
||||
Toggle(String(localized: "alarm.alert_options.vibration", defaultValue: "Vibration"), isOn: $isVibrationEnabled)
|
||||
.tint(AppAccent.primary)
|
||||
.accessibilityIdentifier("alarms.alertOptions.vibrationToggle")
|
||||
|
||||
Toggle("Flash Screen", isOn: $isLightFlashEnabled)
|
||||
Toggle(String(localized: "alarm.alert_options.flash_screen", defaultValue: "Flash Screen"), isOn: $isLightFlashEnabled)
|
||||
.tint(AppAccent.primary)
|
||||
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
HStack {
|
||||
Text("Volume")
|
||||
Text(String(localized: "alarm.alert_options.volume", defaultValue: "Volume"))
|
||||
Spacer()
|
||||
Text("\(Int((volume * 100).rounded()))%")
|
||||
Text(Double(volume), format: .percent.precision(.fractionLength(0)))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
||||
@ -32,11 +32,11 @@ struct EmptyAlarmsView: View {
|
||||
}
|
||||
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
Text("No Alarms Set")
|
||||
Text(String(localized: "alarms.empty.title", defaultValue: "No Alarms Set"))
|
||||
.typography(.title2Bold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text("Create an alarm to wake up gently on your own terms.")
|
||||
Text(String(localized: "alarms.empty.subtitle", defaultValue: "Create an alarm to wake up gently on your own terms."))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -48,7 +48,7 @@ struct EmptyAlarmsView: View {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.symbolEffect(.bounce, options: .nonRepeating)
|
||||
Text("Add Your First Alarm")
|
||||
Text(String(localized: "alarms.empty.cta", defaultValue: "Add Your First Alarm"))
|
||||
}
|
||||
.typography(.bodyEmphasis)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
@ -16,12 +16,15 @@ struct LabelEditView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Alarm Label", text: $label)
|
||||
TextField(
|
||||
String(localized: "alarm.label.placeholder", defaultValue: "Alarm Label"),
|
||||
text: $label
|
||||
)
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
} footer: {
|
||||
Text("Enter a name for your alarm.")
|
||||
Text(String(localized: "alarm.label.footer", defaultValue: "Enter a name for your alarm."))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
@ -29,7 +32,7 @@ struct LabelEditView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
.navigationTitle("Label")
|
||||
.navigationTitle(String(localized: "alarm.label.navigation_title", defaultValue: "Label"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ struct NotificationMessageEditView: View {
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.padding(.vertical, Design.Spacing.xxSmall)
|
||||
} footer: {
|
||||
Text("This message will appear when the alarm rings.")
|
||||
Text(String(localized: "alarm.message.footer", defaultValue: "This message will appear when the alarm rings."))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
@ -30,7 +30,7 @@ struct NotificationMessageEditView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
.navigationTitle("Message")
|
||||
.navigationTitle(String(localized: "alarm.message.navigation_title", defaultValue: "Message"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,14 +16,14 @@ struct RepeatSelectionView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Quick Picks") {
|
||||
quickPickRow(id: "once", title: "Once", weekdays: [])
|
||||
quickPickRow(id: "everyday", title: "Every Day", weekdays: allWeekdays)
|
||||
quickPickRow(id: "weekdays", title: "Weekdays", weekdays: [2, 3, 4, 5, 6])
|
||||
quickPickRow(id: "weekends", title: "Weekends", weekdays: [1, 7])
|
||||
Section(String(localized: "alarm.repeat.quick_picks_section", defaultValue: "Quick Picks")) {
|
||||
quickPickRow(id: "once", title: String(localized: "alarm.repeat.once", defaultValue: "Once"), weekdays: [])
|
||||
quickPickRow(id: "everyday", title: String(localized: "alarm.repeat.every_day", defaultValue: "Every Day"), weekdays: allWeekdays)
|
||||
quickPickRow(id: "weekdays", title: String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays"), weekdays: [2, 3, 4, 5, 6])
|
||||
quickPickRow(id: "weekends", title: String(localized: "alarm.repeat.weekends", defaultValue: "Weekends"), weekdays: [1, 7])
|
||||
}
|
||||
|
||||
Section("Repeat On") {
|
||||
Section(String(localized: "alarm.repeat.repeat_on_section", defaultValue: "Repeat On")) {
|
||||
ForEach(orderedWeekdays, id: \.self) { weekday in
|
||||
HStack {
|
||||
Text(dayName(for: weekday))
|
||||
@ -46,7 +46,7 @@ struct RepeatSelectionView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
.navigationTitle("Repeat")
|
||||
.navigationTitle(String(localized: "alarm.repeat.navigation_title", defaultValue: "Repeat"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
|
||||
@ -17,10 +17,15 @@ struct SnoozeSelectionView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Snooze Duration") {
|
||||
Section(String(localized: "alarm.snooze.duration_section", defaultValue: "Snooze Duration")) {
|
||||
ForEach(snoozeOptions, id: \.self) { duration in
|
||||
HStack {
|
||||
Text("\(duration) minutes")
|
||||
Text(
|
||||
String(
|
||||
localized: "alarm.snooze.duration_minutes",
|
||||
defaultValue: "\(duration) minutes"
|
||||
)
|
||||
)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
if snoozeDuration == duration {
|
||||
@ -39,7 +44,7 @@ struct SnoozeSelectionView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
.navigationTitle("Snooze")
|
||||
.navigationTitle(String(localized: "alarm.snooze.navigation_title", defaultValue: "Snooze"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ struct SoundSelectionView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Alarm Sounds") {
|
||||
Section(String(localized: "alarm.sound.section_title", defaultValue: "Alarm Sounds")) {
|
||||
ForEach(alarmSounds, id: \.id) { sound in
|
||||
HStack {
|
||||
Text(sound.name)
|
||||
@ -50,7 +50,7 @@ struct SoundSelectionView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
.navigationTitle("Sound")
|
||||
.navigationTitle(String(localized: "alarm.sound.navigation_title", defaultValue: "Sound"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
||||
@ -18,7 +18,7 @@ struct TimePickerSection: View {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
DatePicker(
|
||||
"Time",
|
||||
String(localized: "alarm.time_picker.label", defaultValue: "Time"),
|
||||
selection: $selectedTime,
|
||||
displayedComponents: .hourAndMinute
|
||||
)
|
||||
|
||||
@ -46,23 +46,29 @@ struct TimeUntilAlarmSection: View {
|
||||
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
|
||||
if let hours = components.hour, let minutes = components.minute {
|
||||
if hours > 0 {
|
||||
return "Will turn on in \(hours)h \(minutes)m"
|
||||
return String(
|
||||
localized: "alarm.time_until.hours_minutes",
|
||||
defaultValue: "Will turn on in \(hours)h \(minutes)m"
|
||||
)
|
||||
} else if minutes > 0 {
|
||||
return "Will turn on in \(minutes)m"
|
||||
return String(
|
||||
localized: "alarm.time_until.minutes",
|
||||
defaultValue: "Will turn on in \(minutes)m"
|
||||
)
|
||||
} else {
|
||||
return "Will turn on now"
|
||||
return String(localized: "alarm.time_until.now", defaultValue: "Will turn on now")
|
||||
}
|
||||
}
|
||||
|
||||
return "Will turn on tomorrow"
|
||||
return String(localized: "alarm.time_until.tomorrow_fallback", defaultValue: "Will turn on tomorrow")
|
||||
}
|
||||
|
||||
private var dayText: String {
|
||||
let calendar = Calendar.current
|
||||
if calendar.isDateInToday(nextAlarmTime) {
|
||||
return "Today"
|
||||
return String(localized: "calendar.today", defaultValue: "Today")
|
||||
} else if calendar.isDateInTomorrow(nextAlarmTime) {
|
||||
return "Tomorrow"
|
||||
return String(localized: "calendar.tomorrow", defaultValue: "Tomorrow")
|
||||
} else {
|
||||
return nextAlarmTime.formatted(.dateTime.weekday(.wide))
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ struct EditAlarmView: View {
|
||||
Image(systemName: "textformat")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Label")
|
||||
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(alarmLabel)
|
||||
@ -79,7 +79,7 @@ struct EditAlarmView: View {
|
||||
Image(systemName: "message")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Message")
|
||||
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(notificationMessage)
|
||||
@ -95,7 +95,7 @@ struct EditAlarmView: View {
|
||||
Image(systemName: "music.note")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Sound")
|
||||
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(getSoundDisplayName(selectedSoundName))
|
||||
@ -110,7 +110,7 @@ struct EditAlarmView: View {
|
||||
Image(systemName: "repeat")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Repeat")
|
||||
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||
@ -126,10 +126,15 @@ struct EditAlarmView: View {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.frame(width: 24)
|
||||
Text("Snooze")
|
||||
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text("for \(snoozeDuration) min")
|
||||
Text(
|
||||
String(
|
||||
localized: "alarm.editor.snooze_for_minutes",
|
||||
defaultValue: "for \(snoozeDuration) min"
|
||||
)
|
||||
)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
}
|
||||
@ -146,12 +151,12 @@ struct EditAlarmView: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(AppSurface.primary.ignoresSafeArea())
|
||||
}
|
||||
.navigationTitle("Edit Alarm")
|
||||
.navigationTitle(String(localized: "alarm.edit.navigation_title", defaultValue: "Edit Alarm"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
@ -159,7 +164,11 @@ struct EditAlarmView: View {
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(isSaving ? "Saving..." : "Save") {
|
||||
Button(
|
||||
isSaving
|
||||
? String(localized: "common.saving", defaultValue: "Saving...")
|
||||
: String(localized: "common.save", defaultValue: "Save")
|
||||
) {
|
||||
Task {
|
||||
await saveAlarm()
|
||||
}
|
||||
@ -171,8 +180,8 @@ struct EditAlarmView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
|
||||
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
|
||||
} message: {
|
||||
Text(saveErrorMessage)
|
||||
}
|
||||
|
||||
@ -55,24 +55,24 @@ struct ClockSettingsView: View {
|
||||
|
||||
#if DEBUG
|
||||
SettingsSectionHeader(
|
||||
title: "Debug",
|
||||
title: String(localized: "settings.debug.section_title", defaultValue: "Debug"),
|
||||
systemImage: "ant.fill",
|
||||
accentColor: AppStatus.error
|
||||
)
|
||||
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
SettingsNavigationRow(
|
||||
title: "Icon Generator",
|
||||
subtitle: "Generate and save app icon",
|
||||
backgroundColor: AppSurface.primary
|
||||
title: String(localized: "settings.debug.icon_generator.title", defaultValue: "Icon Generator"),
|
||||
subtitle: String(localized: "settings.debug.icon_generator.subtitle", defaultValue: "Generate and save app icon"),
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
|
||||
}
|
||||
|
||||
SettingsNavigationRow(
|
||||
title: "Branding Preview",
|
||||
subtitle: "Preview icon and launch screen",
|
||||
backgroundColor: AppSurface.primary
|
||||
title: String(localized: "settings.debug.branding_preview.title", defaultValue: "Branding Preview"),
|
||||
subtitle: String(localized: "settings.debug.branding_preview.subtitle", defaultValue: "Preview icon and launch screen"),
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
BrandingPreviewView(
|
||||
iconConfig: .noiseClock,
|
||||
@ -82,18 +82,18 @@ struct ClockSettingsView: View {
|
||||
}
|
||||
|
||||
if let onResetOnboarding {
|
||||
Divider()
|
||||
.background(AppBorder.subtle)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
Button {
|
||||
onResetOnboarding()
|
||||
} label: {
|
||||
SettingsCardRow(verticalPadding: Design.Spacing.medium) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
Text("Reset Onboarding")
|
||||
Text(String(localized: "settings.debug.reset_onboarding.title", defaultValue: "Reset Onboarding"))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Text("Show onboarding screens again on next launch")
|
||||
Text(String(localized: "settings.debug.reset_onboarding.subtitle", defaultValue: "Show onboarding screens again on next launch"))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
@ -101,8 +101,7 @@ struct ClockSettingsView: View {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(AppSurface.primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@ -116,7 +115,7 @@ struct ClockSettingsView: View {
|
||||
.padding(.bottom, Design.Spacing.xxxLarge)
|
||||
}
|
||||
.background(AppSurface.primary)
|
||||
.navigationTitle(isPad ? "" : "Settings")
|
||||
.navigationTitle(isPad ? "" : String(localized: "settings.navigation_title", defaultValue: "Settings"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
digitColor = Color(hex: style.digitColorHex) ?? .white
|
||||
|
||||
@ -180,11 +180,26 @@ struct ClockView: View {
|
||||
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Screen: \(Int(size.width))×\(Int(size.height))")
|
||||
Text("Window Insets: L:\(Int(windowInsets.left)) R:\(Int(windowInsets.right)) T:\(Int(windowInsets.top)) B:\(Int(windowInsets.bottom))")
|
||||
Text("Symmetric Inset: \(Int(symmetricInset))")
|
||||
Text("Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")")
|
||||
Text("Orientation: \(isLandscape ? "Landscape" : "Portrait")")
|
||||
Text(String(localized: "clock.debug.screen", defaultValue: "Screen: \(Int(size.width))×\(Int(size.height))"))
|
||||
Text(
|
||||
String(
|
||||
localized: "clock.debug.window_insets",
|
||||
defaultValue: "Window Insets: L:\(Int(windowInsets.left)) R:\(Int(windowInsets.right)) T:\(Int(windowInsets.top)) B:\(Int(windowInsets.bottom))"
|
||||
)
|
||||
)
|
||||
Text(String(localized: "clock.debug.symmetric_inset", defaultValue: "Symmetric Inset: \(Int(symmetricInset))"))
|
||||
Text(
|
||||
String(
|
||||
localized: "clock.debug.dynamic_island",
|
||||
defaultValue: "Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")"
|
||||
)
|
||||
)
|
||||
Text(
|
||||
String(
|
||||
localized: "clock.debug.orientation",
|
||||
defaultValue: "Orientation: \(isLandscape ? "Landscape" : "Portrait")"
|
||||
)
|
||||
)
|
||||
}
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.green)
|
||||
|
||||
@ -27,7 +27,7 @@ struct BatteryOverlayView: View {
|
||||
.foregroundStyle(batteryColor)
|
||||
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
Text("\(batteryLevel)%")
|
||||
Text(Double(batteryLevel) / 100, format: .percent.precision(.fractionLength(0)))
|
||||
.foregroundStyle(color)
|
||||
.contentTransition(.numericText())
|
||||
.animation(.snappy(duration: 0.3), value: batteryLevel)
|
||||
|
||||
@ -45,7 +45,7 @@ struct ClockDisplayContainer: View {
|
||||
.animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityIdentifier("clock.timeDisplay")
|
||||
.accessibilityLabel("Current time")
|
||||
.accessibilityLabel(String(localized: "clock.accessibility.current_time", defaultValue: "Current time"))
|
||||
.accessibilityValue(accessibilityTimeValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ struct ClockToolbar: View {
|
||||
var body: some View {
|
||||
let isPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||
EmptyView()
|
||||
.navigationTitle(isDisplayMode || isPad ? "" : "Clock")
|
||||
.navigationTitle(isDisplayMode || isPad ? "" : String(localized: "clock.navigation_title", defaultValue: "Clock"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(isDisplayMode)
|
||||
.toolbar(isDisplayMode ? .hidden : .automatic)
|
||||
|
||||
@ -195,7 +195,7 @@ private struct GlowAnimationModifier: ViewModifier {
|
||||
animationStyle: .spring)
|
||||
.border(Color.black)
|
||||
|
||||
Text(":")
|
||||
Text(String(localized: "clock.time.colon", defaultValue: ":"))
|
||||
.font(.system(size: sharedFontSize))
|
||||
.border(Color.black)
|
||||
|
||||
|
||||
@ -36,7 +36,11 @@ struct NoiseMiniPlayer: View {
|
||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(isPlaying ? "Playing" : "Paused")
|
||||
Text(
|
||||
isPlaying
|
||||
? String(localized: "noise.mini_player.playing", defaultValue: "Playing")
|
||||
: String(localized: "noise.mini_player.paused", defaultValue: "Paused")
|
||||
)
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
.foregroundStyle(color.opacity(0.6))
|
||||
.textCase(.uppercase)
|
||||
|
||||
@ -14,7 +14,7 @@ struct AdvancedAppearanceSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Advanced Appearance",
|
||||
title: String(localized: "settings.advanced_appearance.section_title", defaultValue: "Advanced Appearance"),
|
||||
systemImage: "sparkles",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -22,38 +22,32 @@ struct AdvancedAppearanceSection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsNavigationRow(
|
||||
title: "Digit Animation",
|
||||
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
|
||||
subtitle: style.digitAnimationStyle.displayName,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.digitAnimationStyle,
|
||||
options: DigitAnimationStyle.allCases,
|
||||
title: "Digit Animation",
|
||||
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
|
||||
toString: { $0.displayName }
|
||||
)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Randomize Color",
|
||||
subtitle: "Shift the color every minute",
|
||||
title: String(localized: "settings.advanced_appearance.randomize_color.title", defaultValue: "Randomize Color"),
|
||||
subtitle: String(localized: "settings.advanced_appearance.randomize_color.subtitle", defaultValue: "Shift the color every minute"),
|
||||
isOn: $style.randomizeColor,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsSlider(
|
||||
title: "Glow",
|
||||
subtitle: "Adjust the glow intensity",
|
||||
title: String(localized: "settings.advanced_appearance.glow.title", defaultValue: "Glow"),
|
||||
subtitle: String(localized: "settings.advanced_appearance.glow.subtitle", defaultValue: "Adjust the glow intensity"),
|
||||
value: $style.glowIntensity,
|
||||
in: 0.0...1.0,
|
||||
step: 0.01,
|
||||
@ -61,14 +55,11 @@ struct AdvancedAppearanceSection: View {
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsSlider(
|
||||
title: "Clock Opacity",
|
||||
subtitle: "Set the clock transparency",
|
||||
title: String(localized: "settings.advanced_appearance.clock_opacity.title", defaultValue: "Clock Opacity"),
|
||||
subtitle: String(localized: "settings.advanced_appearance.clock_opacity.subtitle", defaultValue: "Set the clock transparency"),
|
||||
value: $style.clockOpacity,
|
||||
in: 0.0...1.0,
|
||||
step: 0.01,
|
||||
@ -78,7 +69,7 @@ struct AdvancedAppearanceSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
Text("Fine-tune the visual appearance of your clock.")
|
||||
Text(String(localized: "settings.advanced_appearance.footer", defaultValue: "Fine-tune the visual appearance of your clock."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ struct AdvancedDisplaySection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Advanced Display",
|
||||
title: String(localized: "settings.advanced_display.section_title", defaultValue: "Advanced Display"),
|
||||
systemImage: "eye",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -22,57 +22,51 @@ struct AdvancedDisplaySection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsToggle(
|
||||
title: "Keep Awake",
|
||||
subtitle: "Prevent sleep in display mode",
|
||||
title: String(localized: "settings.advanced_display.keep_awake.title", defaultValue: "Keep Awake"),
|
||||
subtitle: String(localized: "settings.advanced_display.keep_awake.subtitle", defaultValue: "Prevent sleep in display mode"),
|
||||
isOn: $style.keepAwake,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
.accessibilityIdentifier("settings.keepAwake.toggle")
|
||||
|
||||
if style.autoBrightness {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
HStack {
|
||||
Text("Current Brightness")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
Text("\(Int(style.effectiveBrightness * 100))%")
|
||||
SettingsLabelValueRow(
|
||||
title: String(localized: "settings.advanced_display.current_brightness", defaultValue: "Current Brightness"),
|
||||
verticalPadding: Design.Spacing.medium
|
||||
) {
|
||||
Text(style.effectiveBrightness, format: .percent.precision(.fractionLength(0)))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.contentTransition(.numericText())
|
||||
.animation(.snappy(duration: 0.3), value: style.effectiveBrightness)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open.")
|
||||
Text(String(localized: "settings.advanced_display.footer", defaultValue: "Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
|
||||
SettingsSectionHeader(
|
||||
title: "Focus Modes",
|
||||
title: String(localized: "settings.focus_modes.section_title", defaultValue: "Focus Modes"),
|
||||
systemImage: "moon.zzz.fill",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
SettingsToggle(
|
||||
title: "Respect Focus Modes",
|
||||
subtitle: "Follow Do Not Disturb rules",
|
||||
title: String(localized: "settings.focus_modes.respect_focus.title", defaultValue: "Respect Focus Modes"),
|
||||
subtitle: String(localized: "settings.focus_modes.respect_focus.subtitle", defaultValue: "Follow Do Not Disturb rules"),
|
||||
isOn: $style.respectFocusModes,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
.accessibilityIdentifier("settings.respectFocus.toggle")
|
||||
}
|
||||
|
||||
Text("Control how the app behaves when Focus modes are active.")
|
||||
Text(String(localized: "settings.focus_modes.footer", defaultValue: "Control how the app behaves when Focus modes are active."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ struct BasicAppearanceSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Colors",
|
||||
title: String(localized: "settings.colors.section_title", defaultValue: "Colors"),
|
||||
systemImage: "paintpalette.fill",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -24,16 +24,16 @@ struct BasicAppearanceSection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsNavigationRow(
|
||||
title: "Color Theme",
|
||||
subtitle: style.selectedColorTheme,
|
||||
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
|
||||
subtitle: localizedThemeName(for: style.selectedColorTheme),
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.selectedColorTheme,
|
||||
options: ClockStyle.availableColorThemes().map { $0.0 },
|
||||
title: "Color Theme",
|
||||
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
|
||||
toString: { theme in
|
||||
ClockStyle.availableColorThemes().first(where: { $0.0 == theme })?.1 ?? theme
|
||||
localizedThemeName(for: theme)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -46,30 +46,32 @@ struct BasicAppearanceSection: View {
|
||||
}
|
||||
|
||||
if style.selectedColorTheme == "Custom" {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
|
||||
SettingsCardRow {
|
||||
ColorPicker(
|
||||
String(localized: "settings.colors.digit_color", defaultValue: "Digit Color"),
|
||||
selection: $digitColor,
|
||||
supportsOpacity: false
|
||||
)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
|
||||
SettingsCardRow {
|
||||
ColorPicker(
|
||||
String(localized: "settings.colors.background_color", defaultValue: "Background Color"),
|
||||
selection: $backgroundColor,
|
||||
supportsOpacity: true
|
||||
)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Choose your favorite color theme or create a custom look.")
|
||||
Text(String(localized: "settings.colors.footer", defaultValue: "Choose your favorite color theme or create a custom look."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
@ -85,33 +87,32 @@ struct BasicAppearanceSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the color for a theme
|
||||
private func themeColor(for theme: String) -> Color {
|
||||
private func localizedThemeName(for theme: String) -> String {
|
||||
switch theme {
|
||||
case "Custom":
|
||||
return .gray
|
||||
return String(localized: "settings.colors.theme.custom", defaultValue: "Custom")
|
||||
case "Night":
|
||||
return .white
|
||||
return String(localized: "settings.colors.theme.night", defaultValue: "Night")
|
||||
case "Day":
|
||||
return .black
|
||||
return String(localized: "settings.colors.theme.day", defaultValue: "Day")
|
||||
case "Red":
|
||||
return .red
|
||||
return String(localized: "settings.colors.theme.red", defaultValue: "Red")
|
||||
case "Orange":
|
||||
return .orange
|
||||
return String(localized: "settings.colors.theme.orange", defaultValue: "Orange")
|
||||
case "Yellow":
|
||||
return .yellow
|
||||
return String(localized: "settings.colors.theme.yellow", defaultValue: "Yellow")
|
||||
case "Green":
|
||||
return .green
|
||||
return String(localized: "settings.colors.theme.green", defaultValue: "Green")
|
||||
case "Blue":
|
||||
return .blue
|
||||
return String(localized: "settings.colors.theme.blue", defaultValue: "Blue")
|
||||
case "Purple":
|
||||
return .purple
|
||||
return String(localized: "settings.colors.theme.purple", defaultValue: "Purple")
|
||||
case "Pink":
|
||||
return .pink
|
||||
return String(localized: "settings.colors.theme.pink", defaultValue: "Pink")
|
||||
case "White":
|
||||
return .white
|
||||
return String(localized: "settings.colors.theme.white", defaultValue: "White")
|
||||
default:
|
||||
return .gray
|
||||
return theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ struct BasicDisplaySection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Display",
|
||||
title: String(localized: "settings.display.section_title", defaultValue: "Display"),
|
||||
systemImage: "display",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -22,59 +22,47 @@ struct BasicDisplaySection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsToggle(
|
||||
title: "24‑Hour Format",
|
||||
subtitle: "Use military time",
|
||||
title: String(localized: "settings.display.use_24_hour.title", defaultValue: "24‑Hour Format"),
|
||||
subtitle: String(localized: "settings.display.use_24_hour.subtitle", defaultValue: "Use military time"),
|
||||
isOn: $style.use24Hour,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Show Seconds",
|
||||
subtitle: "Display seconds in the clock",
|
||||
title: String(localized: "settings.display.show_seconds.title", defaultValue: "Show Seconds"),
|
||||
subtitle: String(localized: "settings.display.show_seconds.subtitle", defaultValue: "Display seconds in the clock"),
|
||||
isOn: $style.showSeconds,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
if !style.use24Hour {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Show AM/PM",
|
||||
subtitle: "Add an AM/PM indicator",
|
||||
title: String(localized: "settings.display.show_ampm.title", defaultValue: "Show AM/PM"),
|
||||
subtitle: String(localized: "settings.display.show_ampm.subtitle", defaultValue: "Add an AM/PM indicator"),
|
||||
isOn: $style.showAmPm,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Auto Brightness",
|
||||
subtitle: "Adapt brightness to ambient light",
|
||||
title: String(localized: "settings.display.auto_brightness.title", defaultValue: "Auto Brightness"),
|
||||
subtitle: String(localized: "settings.display.auto_brightness.subtitle", defaultValue: "Adapt brightness to ambient light"),
|
||||
isOn: $style.autoBrightness,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Horizontal Mode",
|
||||
subtitle: "Force a wide layout in portrait",
|
||||
title: String(localized: "settings.display.horizontal_mode.title", defaultValue: "Horizontal Mode"),
|
||||
subtitle: String(localized: "settings.display.horizontal_mode.subtitle", defaultValue: "Force a wide layout in portrait"),
|
||||
isOn: $style.forceHorizontalMode,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -82,7 +70,7 @@ struct BasicDisplaySection: View {
|
||||
}
|
||||
}
|
||||
|
||||
Text("Basic display settings for your clock.")
|
||||
Text(String(localized: "settings.display.footer", defaultValue: "Basic display settings for your clock."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ struct FontSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Font",
|
||||
title: String(localized: "settings.font.section_title", defaultValue: "Font"),
|
||||
systemImage: "textformat",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -40,14 +40,14 @@ struct FontSection: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Font Family
|
||||
SettingsNavigationRow(
|
||||
title: "Family",
|
||||
title: String(localized: "settings.font.family.title", defaultValue: "Family"),
|
||||
subtitle: style.fontFamily.rawValue,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.fontFamily,
|
||||
options: sortedFontFamilies,
|
||||
title: "Font Family",
|
||||
title: String(localized: "settings.font.family.selection_title", defaultValue: "Font Family"),
|
||||
toString: { $0.rawValue }
|
||||
)
|
||||
}
|
||||
@ -62,61 +62,49 @@ struct FontSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
// Font Weight
|
||||
SettingsNavigationRow(
|
||||
title: "Weight",
|
||||
title: String(localized: "settings.font.weight.title", defaultValue: "Weight"),
|
||||
subtitle: style.fontWeight.rawValue,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.fontWeight,
|
||||
options: availableWeights,
|
||||
title: "Font Weight",
|
||||
title: String(localized: "settings.font.weight.selection_title", defaultValue: "Font Weight"),
|
||||
toString: { $0.rawValue }
|
||||
)
|
||||
}
|
||||
|
||||
if style.fontFamily == .system {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
// Font Design
|
||||
SettingsNavigationRow(
|
||||
title: "Design",
|
||||
title: String(localized: "settings.font.design.title", defaultValue: "Design"),
|
||||
subtitle: style.fontDesign.rawValue,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.fontDesign,
|
||||
options: Font.Design.allCases,
|
||||
title: "Font Design",
|
||||
title: String(localized: "settings.font.design.selection_title", defaultValue: "Font Design"),
|
||||
toString: { $0.rawValue }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
HStack {
|
||||
Text("Preview")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
Spacer()
|
||||
Text("12:34")
|
||||
SettingsLabelValueRow(
|
||||
title: String(localized: "settings.font.preview.title", defaultValue: "Preview")
|
||||
) {
|
||||
Text(String(localized: "settings.font.preview.sample_time", defaultValue: "12:34"))
|
||||
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ struct NightModeSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Night Mode",
|
||||
title: String(localized: "settings.night_mode.section_title", defaultValue: "Night Mode"),
|
||||
systemImage: "moon.stars.fill",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -22,33 +22,27 @@ struct NightModeSection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsToggle(
|
||||
title: "Enable Night Mode",
|
||||
subtitle: "Use a red clock for low light",
|
||||
title: String(localized: "settings.night_mode.enable.title", defaultValue: "Enable Night Mode"),
|
||||
subtitle: String(localized: "settings.night_mode.enable.subtitle", defaultValue: "Use a red clock for low light"),
|
||||
isOn: $style.nightModeEnabled,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Auto Night Mode",
|
||||
subtitle: "Trigger based on ambient light",
|
||||
title: String(localized: "settings.night_mode.auto.title", defaultValue: "Auto Night Mode"),
|
||||
subtitle: String(localized: "settings.night_mode.auto.subtitle", defaultValue: "Trigger based on ambient light"),
|
||||
isOn: $style.autoNightMode,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
if style.autoNightMode {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsSlider(
|
||||
title: "Light Threshold",
|
||||
subtitle: "Lower values activate sooner",
|
||||
title: String(localized: "settings.night_mode.light_threshold.title", defaultValue: "Light Threshold"),
|
||||
subtitle: String(localized: "settings.night_mode.light_threshold.subtitle", defaultValue: "Lower values activate sooner"),
|
||||
value: $style.ambientLightThreshold,
|
||||
in: 0.1...0.8,
|
||||
step: 0.01,
|
||||
@ -57,71 +51,53 @@ struct NightModeSection: View {
|
||||
)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Scheduled Night Mode",
|
||||
subtitle: "Enable on a daily schedule",
|
||||
title: String(localized: "settings.night_mode.scheduled.title", defaultValue: "Scheduled Night Mode"),
|
||||
subtitle: String(localized: "settings.night_mode.scheduled.subtitle", defaultValue: "Enable on a daily schedule"),
|
||||
isOn: $style.scheduledNightMode,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
if style.scheduledNightMode {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
HStack {
|
||||
Text("Start Time")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
TimePickerView(timeString: $style.nightModeStartTime)
|
||||
SettingsLabelValueRow(
|
||||
title: String(localized: "settings.night_mode.start_time", defaultValue: "Start Time"),
|
||||
verticalPadding: Design.Spacing.medium
|
||||
) {
|
||||
SettingsTimePicker(timeString: $style.nightModeStartTime, accentColor: AppAccent.primary)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
HStack {
|
||||
Text("End Time")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
TimePickerView(timeString: $style.nightModeEndTime)
|
||||
SettingsLabelValueRow(
|
||||
title: String(localized: "settings.night_mode.end_time", defaultValue: "End Time"),
|
||||
verticalPadding: Design.Spacing.medium
|
||||
) {
|
||||
SettingsTimePicker(timeString: $style.nightModeEndTime, accentColor: AppAccent.primary)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
}
|
||||
|
||||
if style.isNightModeActive {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsCardRow {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "moon.fill")
|
||||
.foregroundStyle(AppStatus.error)
|
||||
Text("Night Mode Active")
|
||||
Text(String(localized: "settings.night_mode.active", defaultValue: "Night Mode Active"))
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(AppStatus.error)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Night mode displays the clock in red to reduce eye strain in low light environments.")
|
||||
Text(String(localized: "settings.night_mode.footer", defaultValue: "Night mode displays the clock in red to reduce eye strain in low light environments."))
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ struct OverlaySection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
SettingsSectionHeader(
|
||||
title: "Overlays",
|
||||
title: String(localized: "settings.overlays.section_title", defaultValue: "Overlays"),
|
||||
systemImage: "rectangle.on.rectangle.angled",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -24,63 +24,51 @@ struct OverlaySection: View {
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
SettingsToggle(
|
||||
title: "Battery Level",
|
||||
subtitle: "Show battery percentage",
|
||||
title: String(localized: "settings.overlays.battery_level.title", defaultValue: "Battery Level"),
|
||||
subtitle: String(localized: "settings.overlays.battery_level.subtitle", defaultValue: "Show battery percentage"),
|
||||
isOn: $style.showBattery,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Date",
|
||||
subtitle: "Display the current date",
|
||||
title: String(localized: "settings.overlays.date.title", defaultValue: "Date"),
|
||||
subtitle: String(localized: "settings.overlays.date.subtitle", defaultValue: "Display the current date"),
|
||||
isOn: $style.showDate,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Next Alarm",
|
||||
subtitle: "Show your next scheduled alarm",
|
||||
title: String(localized: "settings.overlays.next_alarm.title", defaultValue: "Next Alarm"),
|
||||
subtitle: String(localized: "settings.overlays.next_alarm.subtitle", defaultValue: "Show your next scheduled alarm"),
|
||||
isOn: $style.showNextAlarm,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsToggle(
|
||||
title: "Noise Controls",
|
||||
subtitle: "Mini-player for white noise",
|
||||
title: String(localized: "settings.overlays.noise_controls.title", defaultValue: "Noise Controls"),
|
||||
subtitle: String(localized: "settings.overlays.noise_controls.subtitle", defaultValue: "Mini-player for white noise"),
|
||||
isOn: $style.showNoiseControls,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
if style.showDate {
|
||||
Rectangle()
|
||||
.fill(AppBorder.subtle)
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsNavigationRow(
|
||||
title: "Date Format",
|
||||
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
|
||||
subtitle: style.dateFormat,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $style.dateFormat,
|
||||
options: dateFormats.map { $0.1 },
|
||||
title: "Date Format",
|
||||
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
|
||||
toString: { format in
|
||||
dateFormats.first(where: { $0.1 == format })?.0 ?? format
|
||||
}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
//
|
||||
// TimePickerView.swift
|
||||
// TheNoiseClock
|
||||
//
|
||||
// Created by Matt Bruce on 9/7/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
struct TimePickerView: View {
|
||||
@Binding var timeString: String
|
||||
@State private var selectedTime = Date()
|
||||
|
||||
var body: some View {
|
||||
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
|
||||
.labelsHidden()
|
||||
.tint(AppAccent.primary)
|
||||
.onAppear {
|
||||
updateSelectedTimeFromString()
|
||||
}
|
||||
.onChange(of: selectedTime) { _, newTime in
|
||||
updateStringFromTime(newTime)
|
||||
}
|
||||
.onChange(of: timeString) { _, _ in
|
||||
updateSelectedTimeFromString()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectedTimeFromString() {
|
||||
let components = timeString.split(separator: ":")
|
||||
guard components.count == 2,
|
||||
let hour = Int(components[0]),
|
||||
let minute = Int(components[1]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
let dateComponents = calendar.dateComponents([.year, .month, .day], from: now)
|
||||
|
||||
var newComponents = dateComponents
|
||||
newComponents.hour = hour
|
||||
newComponents.minute = minute
|
||||
|
||||
if let newDate = calendar.date(from: newComponents) {
|
||||
selectedTime = newDate
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStringFromTime(_ time: Date) {
|
||||
let calendar = Calendar.current
|
||||
let hour = calendar.component(.hour, from: time)
|
||||
let minute = calendar.component(.minute, from: time)
|
||||
timeString = String(format: "%02d:%02d", hour, minute)
|
||||
}
|
||||
}
|
||||
@ -229,12 +229,22 @@ struct TimeDisplayView: View {
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Container: \(Int(containerSize.width))×\(Int(containerSize.height))")
|
||||
Text("Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))")
|
||||
Text("Font Size: \(Int(fontSize))")
|
||||
Text("Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))")
|
||||
Text("Portrait: \(portrait ? "true" : "false")")
|
||||
Text("Family: \(fontFamily.rawValue)")
|
||||
Text(
|
||||
String(
|
||||
localized: "clock.debug.container_size",
|
||||
defaultValue: "Container: \(Int(containerSize.width))×\(Int(containerSize.height))"
|
||||
)
|
||||
)
|
||||
Text(
|
||||
String(
|
||||
localized: "clock.debug.digit_slot_size",
|
||||
defaultValue: "Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))"
|
||||
)
|
||||
)
|
||||
Text(String(localized: "clock.debug.font_size", defaultValue: "Font Size: \(Int(fontSize))"))
|
||||
Text(String(localized: "clock.debug.grid", defaultValue: "Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))"))
|
||||
Text(String(localized: "clock.debug.portrait", defaultValue: "Portrait: \(portrait ? "true" : "false")"))
|
||||
Text(String(localized: "clock.debug.font_family", defaultValue: "Family: \(fontFamily.rawValue)"))
|
||||
}
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.yellow)
|
||||
|
||||
@ -134,7 +134,7 @@ struct CategoryTab: View {
|
||||
.styled(.subheadingEmphasis)
|
||||
|
||||
if count > 0 {
|
||||
Text("\(count)")
|
||||
Text(count, format: .number)
|
||||
.styled(.caption, emphasis: .secondary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
|
||||
@ -62,7 +62,11 @@ struct SoundControlView: View {
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text(isPlaying ? "Stop Sound" : "Play Sound")
|
||||
Text(
|
||||
isPlaying
|
||||
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
|
||||
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
|
||||
)
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
@ -79,7 +83,11 @@ struct SoundControlView: View {
|
||||
.animation(.easeInOut(duration: 0.2), value: isPlaying)
|
||||
.animation(.easeInOut(duration: 0.2), value: selectedSound)
|
||||
.accessibilityIdentifier("noise.playStopButton")
|
||||
.accessibilityLabel(isPlaying ? "Stop Sound" : "Play Sound")
|
||||
.accessibilityLabel(
|
||||
isPlaying
|
||||
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
|
||||
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: 400) // Reasonable max width for iPad
|
||||
.padding(Design.Spacing.medium)
|
||||
|
||||
@ -44,7 +44,10 @@ struct NoiseView: View {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
|
||||
TextField("Search sounds", text: $searchText)
|
||||
TextField(
|
||||
String(localized: "noise.search.placeholder", defaultValue: "Search sounds"),
|
||||
text: $searchText
|
||||
)
|
||||
.textFieldStyle(.plain)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.accessibilityIdentifier("noise.searchField")
|
||||
@ -79,7 +82,7 @@ struct NoiseView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Noise")
|
||||
.navigationTitle(String(localized: "noise.navigation_title", defaultValue: "Noise"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.animation(.easeInOut(duration: 0.3), value: selectedSound)
|
||||
.accessibilityIdentifier("noise.screen")
|
||||
@ -125,11 +128,16 @@ struct NoiseView: View {
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("Ready for Sleep?")
|
||||
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
|
||||
.typography(.title3Bold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text("Select a soothing sound below to begin your relaxation journey.")
|
||||
Text(
|
||||
String(
|
||||
localized: "noise.empty.subtitle_portrait",
|
||||
defaultValue: "Select a soothing sound below to begin your relaxation journey."
|
||||
)
|
||||
)
|
||||
.typography(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -170,7 +178,7 @@ struct NoiseView: View {
|
||||
// Left side: Player controls
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
if !isPad {
|
||||
Text("Ambient Sounds")
|
||||
Text(String(localized: "noise.ambient_sounds", defaultValue: "Ambient Sounds"))
|
||||
.sectionTitleStyle()
|
||||
}
|
||||
|
||||
@ -191,11 +199,11 @@ struct NoiseView: View {
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("Ready for Sleep?")
|
||||
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
|
||||
.typography(.title3Bold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text("Select a soothing sound to begin.")
|
||||
Text(String(localized: "noise.empty.subtitle_landscape", defaultValue: "Select a soothing sound to begin."))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@ -37,7 +37,11 @@ struct OnboardingBottomControls: View {
|
||||
onSkip()
|
||||
}
|
||||
} label: {
|
||||
Text(currentPage == 0 ? "Skip" : "Back")
|
||||
Text(
|
||||
currentPage == 0
|
||||
? String(localized: "onboarding.controls.skip", defaultValue: "Skip")
|
||||
: String(localized: "onboarding.controls.back", defaultValue: "Back")
|
||||
)
|
||||
.typography(.bodyEmphasis)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -52,7 +56,11 @@ struct OnboardingBottomControls: View {
|
||||
onFinish()
|
||||
}
|
||||
} label: {
|
||||
Text(currentPage == totalPages - 1 ? "Get Started" : "Next")
|
||||
Text(
|
||||
currentPage == totalPages - 1
|
||||
? String(localized: "onboarding.controls.get_started", defaultValue: "Get Started")
|
||||
: String(localized: "onboarding.controls.next", defaultValue: "Next")
|
||||
)
|
||||
.typography(.bodyEmphasis)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@ -25,22 +25,22 @@ struct OnboardingGetStartedPage: View {
|
||||
.foregroundStyle(AppStatus.success)
|
||||
}
|
||||
|
||||
Text("You're ready!")
|
||||
Text(String(localized: "onboarding.get_started.title", defaultValue: "You're ready!"))
|
||||
.typography(.heroBold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text("Your alarms will work even in silent mode and Focus mode. The interface will automatically fade out to give you a clean view of the time!")
|
||||
Text(String(localized: "onboarding.get_started.subtitle", defaultValue: "Your alarms will work even in silent mode and Focus mode. The interface will automatically fade out to give you a clean view of the time!"))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
OnboardingFeatureRow(icon: "alarm.fill", text: "Create your first alarm")
|
||||
OnboardingFeatureRow(icon: "repeat", text: "Set repeat days (weekdays/weekends)")
|
||||
OnboardingFeatureRow(icon: "slider.horizontal.3", text: "Customize vibration, flash, and volume")
|
||||
OnboardingFeatureRow(icon: "clock.fill", text: "Wait 5s for full screen")
|
||||
OnboardingFeatureRow(icon: "speaker.wave.2", text: "Tap Noise to play sounds")
|
||||
OnboardingFeatureRow(icon: "alarm.fill", text: String(localized: "onboarding.get_started.feature_first_alarm", defaultValue: "Create your first alarm"))
|
||||
OnboardingFeatureRow(icon: "repeat", text: String(localized: "onboarding.get_started.feature_repeat_days", defaultValue: "Set repeat days (weekdays/weekends)"))
|
||||
OnboardingFeatureRow(icon: "slider.horizontal.3", text: String(localized: "onboarding.get_started.feature_alert_options", defaultValue: "Customize vibration, flash, and volume"))
|
||||
OnboardingFeatureRow(icon: "clock.fill", text: String(localized: "onboarding.get_started.feature_full_screen", defaultValue: "Wait 5s for full screen"))
|
||||
OnboardingFeatureRow(icon: "speaker.wave.2", text: String(localized: "onboarding.get_started.feature_noise", defaultValue: "Tap Noise to play sounds"))
|
||||
}
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
|
||||
|
||||
@ -33,23 +33,23 @@ struct OnboardingPermissionsPage: View {
|
||||
.symbolEffect(.pulse, options: .repeating)
|
||||
}
|
||||
|
||||
Text("Alarms that actually work")
|
||||
Text(String(localized: "onboarding.permissions.title", defaultValue: "Alarms that actually work"))
|
||||
.typography(.heroBold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
|
||||
Text("Works in silent mode, Focus mode, and even when your phone is locked. You can then set repeat days and customize alert behavior.")
|
||||
Text(String(localized: "onboarding.permissions.subtitle", defaultValue: "Works in silent mode, Focus mode, and even when your phone is locked. You can then set repeat days and customize alert behavior."))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
OnboardingFeatureRow(icon: "moon.zzz.fill", text: "Cuts through Do Not Disturb")
|
||||
OnboardingFeatureRow(icon: "lock.iphone", text: "Shows countdown on Lock Screen")
|
||||
OnboardingFeatureRow(icon: "iphone.badge.play", text: "Works when app is closed")
|
||||
OnboardingFeatureRow(icon: "moon.zzz.fill", text: String(localized: "onboarding.permissions.feature_dnd", defaultValue: "Cuts through Do Not Disturb"))
|
||||
OnboardingFeatureRow(icon: "lock.iphone", text: String(localized: "onboarding.permissions.feature_lock_screen", defaultValue: "Shows countdown on Lock Screen"))
|
||||
OnboardingFeatureRow(icon: "iphone.badge.play", text: String(localized: "onboarding.permissions.feature_app_closed", defaultValue: "Works when app is closed"))
|
||||
}
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
|
||||
@ -76,7 +76,7 @@ struct OnboardingPermissionsPage: View {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 24))
|
||||
Text("Alarms enabled!")
|
||||
Text(String(localized: "onboarding.permissions.enabled", defaultValue: "Alarms enabled!"))
|
||||
}
|
||||
.foregroundStyle(AppStatus.success)
|
||||
.typography(.bodyEmphasis)
|
||||
@ -89,7 +89,7 @@ struct OnboardingPermissionsPage: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "alarm.fill")
|
||||
Text("Enable Alarms")
|
||||
Text(String(localized: "onboarding.permissions.enable_alarms", defaultValue: "Enable Alarms"))
|
||||
}
|
||||
.typography(.bodyEmphasis)
|
||||
.foregroundStyle(.white)
|
||||
@ -106,7 +106,7 @@ struct OnboardingPermissionsPage: View {
|
||||
|
||||
private var keepAwakeSection: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
Text("Want the clock always visible?")
|
||||
Text(String(localized: "onboarding.permissions.keep_awake_prompt", defaultValue: "Want the clock always visible?"))
|
||||
.typography(.callout)
|
||||
.foregroundStyle(AppTextColors.tertiary)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -116,7 +116,11 @@ struct OnboardingPermissionsPage: View {
|
||||
} label: {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: keepAwakeEnabled ? "checkmark.circle.fill" : "bolt.fill")
|
||||
Text(keepAwakeEnabled ? "Keep Awake Enabled" : "Enable Keep Awake")
|
||||
Text(
|
||||
keepAwakeEnabled
|
||||
? String(localized: "onboarding.permissions.keep_awake_enabled", defaultValue: "Keep Awake Enabled")
|
||||
: String(localized: "onboarding.permissions.enable_keep_awake", defaultValue: "Enable Keep Awake")
|
||||
)
|
||||
}
|
||||
.typography(.callout)
|
||||
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)
|
||||
|
||||
@ -20,26 +20,26 @@ struct OnboardingWelcomePage: View {
|
||||
}
|
||||
.padding(.bottom, Design.Spacing.medium)
|
||||
|
||||
Text("The Noise Clock")
|
||||
Text(String(localized: "onboarding.welcome.title", defaultValue: "The Noise Clock"))
|
||||
.typography(.heroBold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
|
||||
Text("Your beautiful bedside companion")
|
||||
Text(String(localized: "onboarding.welcome.subtitle", defaultValue: "Your beautiful bedside companion"))
|
||||
.typography(.title3)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
OnboardingFeatureRow(
|
||||
icon: "moon.stars.fill",
|
||||
text: "Fall asleep to soothing sounds"
|
||||
text: String(localized: "onboarding.welcome.feature_sleep_sounds", defaultValue: "Fall asleep to soothing sounds")
|
||||
)
|
||||
OnboardingFeatureRow(
|
||||
icon: "alarm.fill",
|
||||
text: "Wake up your way with custom alarms"
|
||||
text: String(localized: "onboarding.welcome.feature_custom_alarms", defaultValue: "Wake up your way with custom alarms")
|
||||
)
|
||||
OnboardingFeatureRow(
|
||||
icon: "clock.fill",
|
||||
text: "Automatic full-screen display"
|
||||
text: String(localized: "onboarding.welcome.feature_full_screen", defaultValue: "Automatic full-screen display")
|
||||
)
|
||||
}
|
||||
.padding(.top, Design.Spacing.large)
|
||||
|
||||
4736
TheNoiseClock/Localizable.xcstrings
Normal file
4736
TheNoiseClock/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,83 +0,0 @@
|
||||
//
|
||||
// SettingsSelectionView.swift
|
||||
// TheNoiseClock
|
||||
//
|
||||
// Created by Matt Bruce on 9/8/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
/// A reusable selection view for settings that navigates to a new screen.
|
||||
struct SettingsSelectionView<T: Hashable>: View {
|
||||
@Binding var selection: T
|
||||
let options: [T]
|
||||
let title: String
|
||||
let toString: (T) -> String
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
AppSurface.primary.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
SettingsSectionHeader(
|
||||
title: title,
|
||||
systemImage: "checklist",
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(options, id: \.self) { option in
|
||||
Button(action: {
|
||||
selection = option
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text(toString(option))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
Spacer()
|
||||
if selection == option {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.font(.body.bold())
|
||||
}
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(Color.clear)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if option != options.last {
|
||||
Divider()
|
||||
.background(AppBorder.subtle)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
.padding(.top, Design.Spacing.large)
|
||||
.padding(.bottom, Design.Spacing.xxxLarge)
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
SettingsSelectionView(
|
||||
selection: .constant("Option 1"),
|
||||
options: ["Option 1", "Option 2", "Option 3"],
|
||||
title: "Test Selection",
|
||||
toString: { $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -16,10 +16,14 @@ public enum DigitAnimationStyle: String, CaseIterable, Codable {
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .none: return "None"
|
||||
case .spring: return "Spring"
|
||||
case .bounce: return "Bounce"
|
||||
case .glitch: return "Glitch"
|
||||
case .none:
|
||||
return String(localized: "digit_animation.none.display_name", defaultValue: "None")
|
||||
case .spring:
|
||||
return String(localized: "digit_animation.spring.display_name", defaultValue: "Spring")
|
||||
case .bounce:
|
||||
return String(localized: "digit_animation.bounce.display_name", defaultValue: "Bounce")
|
||||
case .glitch:
|
||||
return String(localized: "digit_animation.glitch.display_name", defaultValue: "Glitch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,12 +21,18 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
|
||||
/// Display name for the category
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .all: return "All"
|
||||
case .colored: return "Colored"
|
||||
case .ambient: return "Ambient"
|
||||
case .nature: return "Nature"
|
||||
case .mechanical: return "Mechanical"
|
||||
case .alarm: return "Alarm Sounds"
|
||||
case .all:
|
||||
return String(localized: "sound_category.all.display_name", defaultValue: "All")
|
||||
case .colored:
|
||||
return String(localized: "sound_category.colored.display_name", defaultValue: "Colored")
|
||||
case .ambient:
|
||||
return String(localized: "sound_category.ambient.display_name", defaultValue: "Ambient")
|
||||
case .nature:
|
||||
return String(localized: "sound_category.nature.display_name", defaultValue: "Nature")
|
||||
case .mechanical:
|
||||
return String(localized: "sound_category.mechanical.display_name", defaultValue: "Mechanical")
|
||||
case .alarm:
|
||||
return String(localized: "sound_category.alarm.display_name", defaultValue: "Alarm Sounds")
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,12 +63,18 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
|
||||
/// Description of the category
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .all: return "All available sounds"
|
||||
case .colored: return "Synthetic noise signals for focus, sleep, and relaxation"
|
||||
case .ambient: return "General ambient sounds"
|
||||
case .nature: return "Natural environmental sounds"
|
||||
case .mechanical: return "Mechanical and electronic sounds"
|
||||
case .alarm: return "Wake-up and notification alarm sounds"
|
||||
case .all:
|
||||
return String(localized: "sound_category.all.description", defaultValue: "All available sounds")
|
||||
case .colored:
|
||||
return String(localized: "sound_category.colored.description", defaultValue: "Synthetic noise signals for focus, sleep, and relaxation")
|
||||
case .ambient:
|
||||
return String(localized: "sound_category.ambient.description", defaultValue: "General ambient sounds")
|
||||
case .nature:
|
||||
return String(localized: "sound_category.nature.description", defaultValue: "Natural environmental sounds")
|
||||
case .mechanical:
|
||||
return String(localized: "sound_category.mechanical.description", defaultValue: "Mechanical and electronic sounds")
|
||||
case .alarm:
|
||||
return String(localized: "sound_category.alarm.description", defaultValue: "Wake-up and notification alarm sounds")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,12 +20,12 @@ struct KeepAwakePrompt: View {
|
||||
.symbolEffect(.bounce, options: .nonRepeating)
|
||||
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
Text("Keep Awake for Alarms")
|
||||
Text(String(localized: "keep_awake.prompt.title", defaultValue: "Keep Awake for Alarms"))
|
||||
.typography(.title2Bold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Enable Keep Awake so your alarm can play loudly and show the full screen while TheNoiseClock stays open.")
|
||||
Text(String(localized: "keep_awake.prompt.subtitle", defaultValue: "Enable Keep Awake so your alarm can play loudly and show the full screen while TheNoiseClock stays open."))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -34,7 +34,7 @@ struct KeepAwakePrompt: View {
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
Button(action: onEnable) {
|
||||
Text("Enable Keep Awake")
|
||||
Text(String(localized: "keep_awake.prompt.enable", defaultValue: "Enable Keep Awake"))
|
||||
.font(Typography.headingEmphasis.font)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -44,7 +44,7 @@ struct KeepAwakePrompt: View {
|
||||
}
|
||||
|
||||
Button(action: onDismiss) {
|
||||
Text("Not Now")
|
||||
Text(String(localized: "common.not_now", defaultValue: "Not Now"))
|
||||
.font(Typography.bodyEmphasis.font)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user