Compare commits

..

No commits in common. "4190c95b840b38638b0832c53264180e3578b832" and "3b45fe2114fde4a2faf9718f3f980d8d9d03a83a" have entirely different histories.

48 changed files with 542 additions and 5214 deletions

3
PRD.md
View File

@ -798,9 +798,6 @@ 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

View File

@ -146,9 +146,6 @@ 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).
---

View File

@ -79,25 +79,25 @@ struct ContentView: View {
private var mainTabView: some View {
TabView(selection: $selectedTab) {
Tab(String(localized: "tab.clock", defaultValue: "Clock"), systemImage: "clock", value: AppTab.clock) {
Tab("Clock", systemImage: "clock", value: AppTab.clock) {
NavigationStack {
ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab)
}
}
Tab(String(localized: "tab.alarms", defaultValue: "Alarms"), systemImage: "alarm", value: AppTab.alarms) {
Tab("Alarms", systemImage: "alarm", value: AppTab.alarms) {
NavigationStack {
AlarmView(viewModel: alarmViewModel)
}
}
Tab(String(localized: "tab.noise", defaultValue: "Noise"), systemImage: "waveform", value: AppTab.noise) {
Tab("Noise", systemImage: "waveform", value: AppTab.noise) {
NavigationStack {
NoiseView()
}
}
Tab(String(localized: "tab.settings", defaultValue: "Settings"), systemImage: "gearshape", value: AppTab.settings) {
Tab("Settings", systemImage: "gearshape", value: AppTab.settings) {
NavigationStack {
ClockSettingsView(
style: clockViewModel.style,

View File

@ -17,15 +17,10 @@ import Foundation
/// Intent to stop an active alarm from the Live Activity or notification.
struct StopAlarmIntent: LiveActivityIntent {
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"
)
)
static let title: LocalizedStringResource = "Stop Alarm"
static let description = IntentDescription("Stops the currently ringing alarm")
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
@Parameter(title: "Alarm ID")
var alarmId: String
static var supportedModes: IntentModes { .background }
@ -53,15 +48,10 @@ struct StopAlarmIntent: LiveActivityIntent {
/// Intent to snooze an active alarm from the Live Activity or notification.
struct SnoozeAlarmIntent: LiveActivityIntent {
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"
)
)
static let title: LocalizedStringResource = "Snooze Alarm"
static let description = IntentDescription("Snoozes the currently ringing alarm")
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
@Parameter(title: "Alarm ID")
var alarmId: String
static var supportedModes: IntentModes { .background }
@ -90,16 +80,11 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
/// Intent to open the app when the user taps the Live Activity.
struct OpenAlarmAppIntent: LiveActivityIntent {
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 title: LocalizedStringResource = "Open TheNoiseClock"
static let description = IntentDescription("Opens the app to the alarm screen")
static let openAppWhenRun = true
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
@Parameter(title: "Alarm ID")
var alarmId: String
init() {
@ -125,9 +110,9 @@ enum AlarmIntentError: Error, LocalizedError {
var errorDescription: String? {
switch self {
case .invalidAlarmID:
return String(localized: "alarm_intent.error.invalid_alarm_id", defaultValue: "Invalid alarm ID")
return "Invalid alarm ID"
case .alarmNotFound:
return String(localized: "alarm_intent.error.alarm_not_found", defaultValue: "Alarm not found")
return "Alarm not found"
}
}
}

View File

@ -43,8 +43,8 @@ struct Alarm: Identifiable, Codable, Equatable {
isEnabled: Bool = true,
repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
label: String = "Alarm",
notificationMessage: String = "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) ?? 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.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm"
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? "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,21 +127,19 @@ struct Alarm: Identifiable, Codable, Equatable {
static func repeatSummary(for weekdays: [Int]) -> String {
let normalized = sanitizedWeekdays(weekdays)
guard !normalized.isEmpty else {
return String(localized: "alarm.repeat.once", defaultValue: "Once")
}
guard !normalized.isEmpty else { return "Once" }
let weekdaySet = Set(normalized)
if weekdaySet.count == 7 {
return String(localized: "alarm.repeat.every_day", defaultValue: "Every day")
return "Every day"
}
if weekdaySet == Set([2, 3, 4, 5, 6]) {
return String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays")
return "Weekdays"
}
if weekdaySet == Set([1, 7]) {
return String(localized: "alarm.repeat.weekends", defaultValue: "Weekends")
return "Weekends"
}
let symbols = Calendar.current.shortWeekdaySymbols

View File

@ -83,14 +83,14 @@ final class AlarmKitService {
// Create the stop button for the alarm
let stopButton = AlarmButton(
text: LocalizedStringResource("alarm.action.stop"),
text: "Stop",
textColor: .red,
systemImageName: "stop.fill"
)
// Create the snooze button (secondary button with countdown behavior)
let snoozeButton = AlarmButton(
text: LocalizedStringResource("alarm.action.snooze"),
text: "Snooze",
textColor: .white,
systemImageName: "moon.zzz"
)
@ -422,15 +422,9 @@ enum AlarmKitError: Error, LocalizedError {
var errorDescription: String? {
switch self {
case .notAuthorized:
return String(
localized: "alarmkit.error.not_authorized",
defaultValue: "AlarmKit is not authorized. Please enable alarm permissions in Settings."
)
return "AlarmKit is not authorized. Please enable alarm permissions in Settings."
case .schedulingFailed(let error):
return String(
localized: "alarmkit.error.scheduling_failed",
defaultValue: "Failed to schedule alarm: \(error.localizedDescription)"
)
return "Failed to schedule alarm: \(error.localizedDescription)"
}
}
}

View File

@ -152,8 +152,8 @@ final class AlarmViewModel {
time: Date,
repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false,
@ -219,9 +219,6 @@ final class AlarmViewModel {
} else {
error.localizedDescription
}
return String(
localized: "alarm.operation.error_message",
defaultValue: "Unable to \(action) alarm. \(detail)"
)
return "Unable to \(action) alarm. \(detail)"
}
}

View File

@ -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 = 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 alarmLabel = "Alarm"
@State private var notificationMessage = "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(String(localized: "alarm.editor.label", defaultValue: "Label"))
Text("Label")
Spacer()
Text(alarmLabel)
.foregroundStyle(.secondary)
@ -57,7 +57,7 @@ struct AddAlarmView: View {
Image(systemName: "message")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
Text("Message")
Spacer()
Text(notificationMessage)
.foregroundStyle(.secondary)
@ -71,7 +71,7 @@ struct AddAlarmView: View {
Image(systemName: "music.note")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
Text("Sound")
Spacer()
Text(getSoundDisplayName(selectedSoundName))
.foregroundStyle(.secondary)
@ -84,7 +84,7 @@ struct AddAlarmView: View {
Image(systemName: "repeat")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
Text("Repeat")
Spacer()
Text(Alarm.repeatSummary(for: repeatWeekdays))
.foregroundStyle(.secondary)
@ -98,14 +98,9 @@ struct AddAlarmView: View {
Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
Text("Snooze")
Spacer()
Text(
String(
localized: "alarm.editor.snooze_for_minutes",
defaultValue: "for \(snoozeDuration) min"
)
)
Text("for \(snoozeDuration) min")
.foregroundStyle(.secondary)
}
}
@ -118,12 +113,12 @@ struct AddAlarmView: View {
}
.listStyle(.insetGrouped)
}
.navigationTitle(String(localized: "alarm.add.navigation_title", defaultValue: "Alarm"))
.navigationTitle("Alarm")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
Button("Cancel") {
isPresented = false
}
.foregroundStyle(AppAccent.primary)
@ -131,11 +126,7 @@ struct AddAlarmView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(
isSaving
? String(localized: "common.saving", defaultValue: "Saving...")
: String(localized: "common.save", defaultValue: "Save")
) {
Button(isSaving ? "Saving..." : "Save") {
Task {
await saveAlarm()
}
@ -147,8 +138,8 @@ struct AddAlarmView: View {
}
}
}
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(saveErrorMessage)
}

View File

@ -71,7 +71,7 @@ struct AlarmView: View {
.frame(maxWidth: .infinity, alignment: .center)
}
}
.navigationTitle(isPad ? "" : String(localized: "alarms.navigation_title", defaultValue: "Alarms"))
.navigationTitle(isPad ? "" : "Alarms")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
@ -107,8 +107,8 @@ struct AlarmView: View {
}
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
.accessibilityIdentifier("alarms.screen")
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $viewModel.isShowingErrorAlert) {
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {
.alert("Alarm Error", isPresented: $viewModel.isShowingErrorAlert) {
Button("OK", role: .cancel) {
viewModel.dismissErrorAlert()
}
} message: {

View File

@ -36,12 +36,7 @@ struct AlarmRowView: View {
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
Text(
String(
localized: "alarm.row.sound_prefix",
defaultValue: "\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
)
)
Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
@ -51,7 +46,7 @@ struct AlarmRowView: View {
.font(.caption2)
.foregroundStyle(AppStatus.warning)
.symbolEffect(.pulse, options: .repeating)
Text(String(localized: "alarm.row.keep_awake_warning", defaultValue: "Foreground only for full alarm sound"))
Text("Foreground only for full alarm sound")
.font(.caption2)
.foregroundStyle(AppTextColors.tertiary)
}
@ -78,23 +73,14 @@ struct AlarmRowView: View {
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
.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")
)
.accessibilityLabel("\(alarm.label), \(alarm.formattedTime())")
.accessibilityValue(alarm.isEnabled ? "Enabled" : "Disabled")
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
onDelete()
} label: {
Label(String(localized: "common.delete", defaultValue: "Delete"), systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
}

View File

@ -15,20 +15,20 @@ struct AlertOptionsSection: View {
@Binding var volume: Float
var body: some View {
Section(String(localized: "alarm.alert_options.section_title", defaultValue: "Alert Options")) {
Toggle(String(localized: "alarm.alert_options.vibration", defaultValue: "Vibration"), isOn: $isVibrationEnabled)
Section("Alert Options") {
Toggle("Vibration", isOn: $isVibrationEnabled)
.tint(AppAccent.primary)
.accessibilityIdentifier("alarms.alertOptions.vibrationToggle")
Toggle(String(localized: "alarm.alert_options.flash_screen", defaultValue: "Flash Screen"), isOn: $isLightFlashEnabled)
Toggle("Flash Screen", isOn: $isLightFlashEnabled)
.tint(AppAccent.primary)
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(String(localized: "alarm.alert_options.volume", defaultValue: "Volume"))
Text("Volume")
Spacer()
Text(Double(volume), format: .percent.precision(.fractionLength(0)))
Text("\(Int((volume * 100).rounded()))%")
.foregroundStyle(.secondary)
}

View File

@ -32,11 +32,11 @@ struct EmptyAlarmsView: View {
}
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "alarms.empty.title", defaultValue: "No Alarms Set"))
Text("No Alarms Set")
.typography(.title2Bold)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "alarms.empty.subtitle", defaultValue: "Create an alarm to wake up gently on your own terms."))
Text("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(String(localized: "alarms.empty.cta", defaultValue: "Add Your First Alarm"))
Text("Add Your First Alarm")
}
.typography(.bodyEmphasis)
.foregroundStyle(.white)

View File

@ -16,15 +16,12 @@ struct LabelEditView: View {
var body: some View {
Form {
Section {
TextField(
String(localized: "alarm.label.placeholder", defaultValue: "Alarm Label"),
text: $label
)
TextField("Alarm Label", text: $label)
.typography(.body)
.foregroundStyle(AppTextColors.primary)
.padding(.vertical, Design.Spacing.small)
} footer: {
Text(String(localized: "alarm.label.footer", defaultValue: "Enter a name for your alarm."))
Text("Enter a name for your alarm.")
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
}
@ -32,7 +29,7 @@ struct LabelEditView: View {
}
.scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea())
.navigationTitle(String(localized: "alarm.label.navigation_title", defaultValue: "Label"))
.navigationTitle("Label")
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -22,7 +22,7 @@ struct NotificationMessageEditView: View {
.foregroundStyle(AppTextColors.primary)
.padding(.vertical, Design.Spacing.xxSmall)
} footer: {
Text(String(localized: "alarm.message.footer", defaultValue: "This message will appear when the alarm rings."))
Text("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(String(localized: "alarm.message.navigation_title", defaultValue: "Message"))
.navigationTitle("Message")
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -16,14 +16,14 @@ struct RepeatSelectionView: View {
var body: some View {
List {
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("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.repeat_on_section", defaultValue: "Repeat On")) {
Section("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(String(localized: "alarm.repeat.navigation_title", defaultValue: "Repeat"))
.navigationTitle("Repeat")
.navigationBarTitleDisplayMode(.inline)
}

View File

@ -17,15 +17,10 @@ struct SnoozeSelectionView: View {
var body: some View {
List {
Section(String(localized: "alarm.snooze.duration_section", defaultValue: "Snooze Duration")) {
Section("Snooze Duration") {
ForEach(snoozeOptions, id: \.self) { duration in
HStack {
Text(
String(
localized: "alarm.snooze.duration_minutes",
defaultValue: "\(duration) minutes"
)
)
Text("\(duration) minutes")
.foregroundStyle(AppTextColors.primary)
Spacer()
if snoozeDuration == duration {
@ -44,7 +39,7 @@ struct SnoozeSelectionView: View {
.listStyle(.insetGrouped)
.scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea())
.navigationTitle(String(localized: "alarm.snooze.navigation_title", defaultValue: "Snooze"))
.navigationTitle("Snooze")
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -23,7 +23,7 @@ struct SoundSelectionView: View {
var body: some View {
List {
Section(String(localized: "alarm.sound.section_title", defaultValue: "Alarm Sounds")) {
Section("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(String(localized: "alarm.sound.navigation_title", defaultValue: "Sound"))
.navigationTitle("Sound")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {

View File

@ -18,7 +18,7 @@ struct TimePickerSection: View {
VStack(spacing: 0) {
DatePicker(
String(localized: "alarm.time_picker.label", defaultValue: "Time"),
"Time",
selection: $selectedTime,
displayedComponents: .hourAndMinute
)

View File

@ -46,29 +46,23 @@ 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 String(
localized: "alarm.time_until.hours_minutes",
defaultValue: "Will turn on in \(hours)h \(minutes)m"
)
return "Will turn on in \(hours)h \(minutes)m"
} else if minutes > 0 {
return String(
localized: "alarm.time_until.minutes",
defaultValue: "Will turn on in \(minutes)m"
)
return "Will turn on in \(minutes)m"
} else {
return String(localized: "alarm.time_until.now", defaultValue: "Will turn on now")
return "Will turn on now"
}
}
return String(localized: "alarm.time_until.tomorrow_fallback", defaultValue: "Will turn on tomorrow")
return "Will turn on tomorrow"
}
private var dayText: String {
let calendar = Calendar.current
if calendar.isDateInToday(nextAlarmTime) {
return String(localized: "calendar.today", defaultValue: "Today")
return "Today"
} else if calendar.isDateInTomorrow(nextAlarmTime) {
return String(localized: "calendar.tomorrow", defaultValue: "Tomorrow")
return "Tomorrow"
} else {
return nextAlarmTime.formatted(.dateTime.weekday(.wide))
}

View File

@ -64,7 +64,7 @@ struct EditAlarmView: View {
Image(systemName: "textformat")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
Text("Label")
.foregroundStyle(AppTextColors.primary)
Spacer()
Text(alarmLabel)
@ -79,7 +79,7 @@ struct EditAlarmView: View {
Image(systemName: "message")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
Text("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(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
Text("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(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
Text("Repeat")
.foregroundStyle(AppTextColors.primary)
Spacer()
Text(Alarm.repeatSummary(for: repeatWeekdays))
@ -126,15 +126,10 @@ struct EditAlarmView: View {
Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
Text("Snooze")
.foregroundStyle(AppTextColors.primary)
Spacer()
Text(
String(
localized: "alarm.editor.snooze_for_minutes",
defaultValue: "for \(snoozeDuration) min"
)
)
Text("for \(snoozeDuration) min")
.foregroundStyle(AppTextColors.secondary)
}
}
@ -151,12 +146,12 @@ struct EditAlarmView: View {
.scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea())
}
.navigationTitle(String(localized: "alarm.edit.navigation_title", defaultValue: "Edit Alarm"))
.navigationTitle("Edit Alarm")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
Button("Cancel") {
dismiss()
}
.foregroundStyle(AppAccent.primary)
@ -164,11 +159,7 @@ struct EditAlarmView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(
isSaving
? String(localized: "common.saving", defaultValue: "Saving...")
: String(localized: "common.save", defaultValue: "Save")
) {
Button(isSaving ? "Saving..." : "Save") {
Task {
await saveAlarm()
}
@ -180,8 +171,8 @@ struct EditAlarmView: View {
}
}
}
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(saveErrorMessage)
}

View File

@ -55,24 +55,24 @@ struct ClockSettingsView: View {
#if DEBUG
SettingsSectionHeader(
title: String(localized: "settings.debug.section_title", defaultValue: "Debug"),
title: "Debug",
systemImage: "ant.fill",
accentColor: AppStatus.error
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsNavigationRow(
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
title: "Icon Generator",
subtitle: "Generate and save app icon",
backgroundColor: AppSurface.primary
) {
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
}
SettingsNavigationRow(
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
title: "Branding Preview",
subtitle: "Preview icon and launch screen",
backgroundColor: AppSurface.primary
) {
BrandingPreviewView(
iconConfig: .noiseClock,
@ -82,26 +82,27 @@ struct ClockSettingsView: View {
}
if let onResetOnboarding {
SettingsDivider(color: AppBorder.subtle)
Divider()
.background(AppBorder.subtle)
Button {
onResetOnboarding()
} label: {
SettingsCardRow(verticalPadding: Design.Spacing.medium) {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "settings.debug.reset_onboarding.title", defaultValue: "Reset Onboarding"))
.typography(.body)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "settings.debug.reset_onboarding.subtitle", defaultValue: "Show onboarding screens again on next launch"))
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
}
Spacer()
Image(systemName: "arrow.counterclockwise")
.foregroundStyle(AppAccent.primary)
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Reset Onboarding")
.typography(.body)
.foregroundStyle(AppTextColors.primary)
Text("Show onboarding screens again on next launch")
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
}
Spacer()
Image(systemName: "arrow.counterclockwise")
.foregroundStyle(AppAccent.primary)
}
.padding(Design.Spacing.medium)
.background(AppSurface.primary)
}
.buttonStyle(.plain)
}
@ -115,7 +116,7 @@ struct ClockSettingsView: View {
.padding(.bottom, Design.Spacing.xxxLarge)
}
.background(AppSurface.primary)
.navigationTitle(isPad ? "" : String(localized: "settings.navigation_title", defaultValue: "Settings"))
.navigationTitle(isPad ? "" : "Settings")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
digitColor = Color(hex: style.digitColorHex) ?? .white

View File

@ -180,26 +180,11 @@ struct ClockView: View {
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
VStack(alignment: .leading, spacing: 2) {
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")"
)
)
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")")
}
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(.green)

View File

@ -27,7 +27,7 @@ struct BatteryOverlayView: View {
.foregroundStyle(batteryColor)
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
.contentTransition(.symbolEffect(.replace))
Text(Double(batteryLevel) / 100, format: .percent.precision(.fractionLength(0)))
Text("\(batteryLevel)%")
.foregroundStyle(color)
.contentTransition(.numericText())
.animation(.snappy(duration: 0.3), value: batteryLevel)

View File

@ -45,7 +45,7 @@ struct ClockDisplayContainer: View {
.animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode)
.accessibilityElement(children: .ignore)
.accessibilityIdentifier("clock.timeDisplay")
.accessibilityLabel(String(localized: "clock.accessibility.current_time", defaultValue: "Current time"))
.accessibilityLabel("Current time")
.accessibilityValue(accessibilityTimeValue)
}
}

View File

@ -17,7 +17,7 @@ struct ClockToolbar: View {
var body: some View {
let isPad = UIDevice.current.userInterfaceIdiom == .pad
EmptyView()
.navigationTitle(isDisplayMode || isPad ? "" : String(localized: "clock.navigation_title", defaultValue: "Clock"))
.navigationTitle(isDisplayMode || isPad ? "" : "Clock")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(isDisplayMode)
.toolbar(isDisplayMode ? .hidden : .automatic)

View File

@ -195,7 +195,7 @@ private struct GlowAnimationModifier: ViewModifier {
animationStyle: .spring)
.border(Color.black)
Text(String(localized: "clock.time.colon", defaultValue: ":"))
Text(":")
.font(.system(size: sharedFontSize))
.border(Color.black)

View File

@ -36,11 +36,7 @@ struct NoiseMiniPlayer: View {
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
VStack(alignment: .leading, spacing: 0) {
Text(
isPlaying
? String(localized: "noise.mini_player.playing", defaultValue: "Playing")
: String(localized: "noise.mini_player.paused", defaultValue: "Paused")
)
Text(isPlaying ? "Playing" : "Paused")
.font(.system(size: 8, weight: .bold))
.foregroundStyle(color.opacity(0.6))
.textCase(.uppercase)

View File

@ -14,7 +14,7 @@ struct AdvancedAppearanceSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.advanced_appearance.section_title", defaultValue: "Advanced Appearance"),
title: "Advanced Appearance",
systemImage: "sparkles",
accentColor: AppAccent.primary
)
@ -22,32 +22,38 @@ struct AdvancedAppearanceSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsNavigationRow(
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
title: "Digit Animation",
subtitle: style.digitAnimationStyle.displayName,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.digitAnimationStyle,
options: DigitAnimationStyle.allCases,
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
title: "Digit Animation",
toString: { $0.displayName }
)
}
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Randomize Color",
subtitle: "Shift the color every minute",
isOn: $style.randomizeColor,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsSlider(
title: String(localized: "settings.advanced_appearance.glow.title", defaultValue: "Glow"),
subtitle: String(localized: "settings.advanced_appearance.glow.subtitle", defaultValue: "Adjust the glow intensity"),
title: "Glow",
subtitle: "Adjust the glow intensity",
value: $style.glowIntensity,
in: 0.0...1.0,
step: 0.01,
@ -55,11 +61,14 @@ struct AdvancedAppearanceSection: View {
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsSlider(
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"),
title: "Clock Opacity",
subtitle: "Set the clock transparency",
value: $style.clockOpacity,
in: 0.0...1.0,
step: 0.01,
@ -69,7 +78,7 @@ struct AdvancedAppearanceSection: View {
}
}
Text(String(localized: "settings.advanced_appearance.footer", defaultValue: "Fine-tune the visual appearance of your clock."))
Text("Fine-tune the visual appearance of your clock.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}

View File

@ -14,7 +14,7 @@ struct AdvancedDisplaySection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.advanced_display.section_title", defaultValue: "Advanced Display"),
title: "Advanced Display",
systemImage: "eye",
accentColor: AppAccent.primary
)
@ -22,51 +22,57 @@ struct AdvancedDisplaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
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"),
title: "Keep Awake",
subtitle: "Prevent sleep in display mode",
isOn: $style.keepAwake,
accentColor: AppAccent.primary
)
.accessibilityIdentifier("settings.keepAwake.toggle")
if style.autoBrightness {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsLabelValueRow(
title: String(localized: "settings.advanced_display.current_brightness", defaultValue: "Current Brightness"),
verticalPadding: Design.Spacing.medium
) {
Text(style.effectiveBrightness, format: .percent.precision(.fractionLength(0)))
HStack {
Text("Current Brightness")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
Text("\(Int(style.effectiveBrightness * 100))%")
.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(String(localized: "settings.advanced_display.footer", defaultValue: "Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open."))
Text("Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
SettingsSectionHeader(
title: String(localized: "settings.focus_modes.section_title", defaultValue: "Focus Modes"),
title: "Focus Modes",
systemImage: "moon.zzz.fill",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
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"),
title: "Respect Focus Modes",
subtitle: "Follow Do Not Disturb rules",
isOn: $style.respectFocusModes,
accentColor: AppAccent.primary
)
.accessibilityIdentifier("settings.respectFocus.toggle")
}
Text(String(localized: "settings.focus_modes.footer", defaultValue: "Control how the app behaves when Focus modes are active."))
Text("Control how the app behaves when Focus modes are active.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}

View File

@ -16,7 +16,7 @@ struct BasicAppearanceSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.colors.section_title", defaultValue: "Colors"),
title: "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: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
subtitle: localizedThemeName(for: style.selectedColorTheme),
title: "Color Theme",
subtitle: style.selectedColorTheme,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.selectedColorTheme,
options: ClockStyle.availableColorThemes().map { $0.0 },
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
title: "Color Theme",
toString: { theme in
localizedThemeName(for: theme)
ClockStyle.availableColorThemes().first(where: { $0.0 == theme })?.1 ?? theme
}
)
}
@ -46,32 +46,30 @@ struct BasicAppearanceSection: View {
}
if style.selectedColorTheme == "Custom" {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsCardRow {
ColorPicker(
String(localized: "settings.colors.digit_color", defaultValue: "Digit Color"),
selection: $digitColor,
supportsOpacity: false
)
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
.foregroundStyle(AppTextColors.primary)
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsCardRow {
ColorPicker(
String(localized: "settings.colors.background_color", defaultValue: "Background Color"),
selection: $backgroundColor,
supportsOpacity: true
)
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
.foregroundStyle(AppTextColors.primary)
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
}
}
}
Text(String(localized: "settings.colors.footer", defaultValue: "Choose your favorite color theme or create a custom look."))
Text("Choose your favorite color theme or create a custom look.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
@ -87,32 +85,33 @@ struct BasicAppearanceSection: View {
}
}
private func localizedThemeName(for theme: String) -> String {
/// Get the color for a theme
private func themeColor(for theme: String) -> Color {
switch theme {
case "Custom":
return String(localized: "settings.colors.theme.custom", defaultValue: "Custom")
return .gray
case "Night":
return String(localized: "settings.colors.theme.night", defaultValue: "Night")
return .white
case "Day":
return String(localized: "settings.colors.theme.day", defaultValue: "Day")
return .black
case "Red":
return String(localized: "settings.colors.theme.red", defaultValue: "Red")
return .red
case "Orange":
return String(localized: "settings.colors.theme.orange", defaultValue: "Orange")
return .orange
case "Yellow":
return String(localized: "settings.colors.theme.yellow", defaultValue: "Yellow")
return .yellow
case "Green":
return String(localized: "settings.colors.theme.green", defaultValue: "Green")
return .green
case "Blue":
return String(localized: "settings.colors.theme.blue", defaultValue: "Blue")
return .blue
case "Purple":
return String(localized: "settings.colors.theme.purple", defaultValue: "Purple")
return .purple
case "Pink":
return String(localized: "settings.colors.theme.pink", defaultValue: "Pink")
return .pink
case "White":
return String(localized: "settings.colors.theme.white", defaultValue: "White")
return .white
default:
return theme
return .gray
}
}
}

View File

@ -14,7 +14,7 @@ struct BasicDisplaySection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.display.section_title", defaultValue: "Display"),
title: "Display",
systemImage: "display",
accentColor: AppAccent.primary
)
@ -22,47 +22,59 @@ struct BasicDisplaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
title: String(localized: "settings.display.use_24_hour.title", defaultValue: "24Hour Format"),
subtitle: String(localized: "settings.display.use_24_hour.subtitle", defaultValue: "Use military time"),
title: "24Hour Format",
subtitle: "Use military time",
isOn: $style.use24Hour,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Show Seconds",
subtitle: "Display seconds in the clock",
isOn: $style.showSeconds,
accentColor: AppAccent.primary
)
if !style.use24Hour {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Show AM/PM",
subtitle: "Add an AM/PM indicator",
isOn: $style.showAmPm,
accentColor: AppAccent.primary
)
}
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Auto Brightness",
subtitle: "Adapt brightness to ambient light",
isOn: $style.autoBrightness,
accentColor: AppAccent.primary
)
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Horizontal Mode",
subtitle: "Force a wide layout in portrait",
isOn: $style.forceHorizontalMode,
accentColor: AppAccent.primary
)
@ -70,7 +82,7 @@ struct BasicDisplaySection: View {
}
}
Text(String(localized: "settings.display.footer", defaultValue: "Basic display settings for your clock."))
Text("Basic display settings for your clock.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}

View File

@ -31,7 +31,7 @@ struct FontSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.font.section_title", defaultValue: "Font"),
title: "Font",
systemImage: "textformat",
accentColor: AppAccent.primary
)
@ -40,14 +40,14 @@ struct FontSection: View {
VStack(alignment: .leading, spacing: 0) {
// Font Family
SettingsNavigationRow(
title: String(localized: "settings.font.family.title", defaultValue: "Family"),
title: "Family",
subtitle: style.fontFamily.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontFamily,
options: sortedFontFamilies,
title: String(localized: "settings.font.family.selection_title", defaultValue: "Font Family"),
title: "Font Family",
toString: { $0.rawValue }
)
}
@ -62,49 +62,61 @@ struct FontSection: View {
}
}
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
// Font Weight
SettingsNavigationRow(
title: String(localized: "settings.font.weight.title", defaultValue: "Weight"),
title: "Weight",
subtitle: style.fontWeight.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontWeight,
options: availableWeights,
title: String(localized: "settings.font.weight.selection_title", defaultValue: "Font Weight"),
title: "Font Weight",
toString: { $0.rawValue }
)
}
if style.fontFamily == .system {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
// Font Design
SettingsNavigationRow(
title: String(localized: "settings.font.design.title", defaultValue: "Design"),
title: "Design",
subtitle: style.fontDesign.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontDesign,
options: Font.Design.allCases,
title: String(localized: "settings.font.design.selection_title", defaultValue: "Font Design"),
title: "Font Design",
toString: { $0.rawValue }
)
}
}
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsLabelValueRow(
title: String(localized: "settings.font.preview.title", defaultValue: "Preview")
) {
Text(String(localized: "settings.font.preview.sample_time", defaultValue: "12:34"))
HStack {
Text("Preview")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.secondary)
Spacer()
Text("12:34")
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
.foregroundStyle(AppTextColors.primary)
}
.padding(Design.Spacing.medium)
}
}
}

View File

@ -14,7 +14,7 @@ struct NightModeSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.night_mode.section_title", defaultValue: "Night Mode"),
title: "Night Mode",
systemImage: "moon.stars.fill",
accentColor: AppAccent.primary
)
@ -22,27 +22,33 @@ struct NightModeSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
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"),
title: "Enable Night Mode",
subtitle: "Use a red clock for low light",
isOn: $style.nightModeEnabled,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Auto Night Mode",
subtitle: "Trigger based on ambient light",
isOn: $style.autoNightMode,
accentColor: AppAccent.primary
)
if style.autoNightMode {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsSlider(
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"),
title: "Light Threshold",
subtitle: "Lower values activate sooner",
value: $style.ambientLightThreshold,
in: 0.1...0.8,
step: 0.01,
@ -51,53 +57,71 @@ struct NightModeSection: View {
)
}
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Scheduled Night Mode",
subtitle: "Enable on a daily schedule",
isOn: $style.scheduledNightMode,
accentColor: AppAccent.primary
)
if style.scheduledNightMode {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsLabelValueRow(
title: String(localized: "settings.night_mode.start_time", defaultValue: "Start Time"),
verticalPadding: Design.Spacing.medium
) {
SettingsTimePicker(timeString: $style.nightModeStartTime, accentColor: AppAccent.primary)
HStack {
Text("Start Time")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
TimePickerView(timeString: $style.nightModeStartTime)
}
.padding(.vertical, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.medium)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsLabelValueRow(
title: String(localized: "settings.night_mode.end_time", defaultValue: "End Time"),
verticalPadding: Design.Spacing.medium
) {
SettingsTimePicker(timeString: $style.nightModeEndTime, accentColor: AppAccent.primary)
HStack {
Text("End Time")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
TimePickerView(timeString: $style.nightModeEndTime)
}
.padding(.vertical, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.medium)
}
if style.isNightModeActive {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsCardRow {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "moon.fill")
.foregroundStyle(AppStatus.error)
Text(String(localized: "settings.night_mode.active", defaultValue: "Night Mode Active"))
.font(.subheadline.weight(.medium))
.foregroundStyle(AppStatus.error)
Spacer()
}
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "moon.fill")
.foregroundStyle(AppStatus.error)
Text("Night Mode Active")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppStatus.error)
Spacer()
}
.padding(.vertical, Design.Spacing.small)
.padding(.horizontal, Design.Spacing.medium)
}
}
}
Text(String(localized: "settings.night_mode.footer", defaultValue: "Night mode displays the clock in red to reduce eye strain in low light environments."))
Text("Night mode displays the clock in red to reduce eye strain in low light environments.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}

View File

@ -16,7 +16,7 @@ struct OverlaySection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: String(localized: "settings.overlays.section_title", defaultValue: "Overlays"),
title: "Overlays",
systemImage: "rectangle.on.rectangle.angled",
accentColor: AppAccent.primary
)
@ -24,51 +24,63 @@ struct OverlaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
title: String(localized: "settings.overlays.battery_level.title", defaultValue: "Battery Level"),
subtitle: String(localized: "settings.overlays.battery_level.subtitle", defaultValue: "Show battery percentage"),
title: "Battery Level",
subtitle: "Show battery percentage",
isOn: $style.showBattery,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
title: String(localized: "settings.overlays.date.title", defaultValue: "Date"),
subtitle: String(localized: "settings.overlays.date.subtitle", defaultValue: "Display the current date"),
title: "Date",
subtitle: "Display the current date",
isOn: $style.showDate,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Next Alarm",
subtitle: "Show your next scheduled alarm",
isOn: $style.showNextAlarm,
accentColor: AppAccent.primary
)
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
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"),
title: "Noise Controls",
subtitle: "Mini-player for white noise",
isOn: $style.showNoiseControls,
accentColor: AppAccent.primary
)
if style.showDate {
SettingsDivider(color: AppBorder.subtle)
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
SettingsNavigationRow(
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
title: "Date Format",
subtitle: style.dateFormat,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.dateFormat,
options: dateFormats.map { $0.1 },
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
title: "Date Format",
toString: { format in
dateFormats.first(where: { $0.1 == format })?.0 ?? format
}

View File

@ -0,0 +1,57 @@
//
// 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)
}
}

View File

@ -229,22 +229,12 @@ struct TimeDisplayView: View {
)
VStack(alignment: .leading, spacing: 2) {
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)"))
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)")
}
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(.yellow)

View File

@ -134,7 +134,7 @@ struct CategoryTab: View {
.styled(.subheadingEmphasis)
if count > 0 {
Text(count, format: .number)
Text("\(count)")
.styled(.caption, emphasis: .secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)

View File

@ -62,11 +62,7 @@ struct SoundControlView: View {
.font(.title2.weight(.semibold))
.foregroundStyle(.white)
Text(
isPlaying
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
)
Text(isPlaying ? "Stop Sound" : "Play Sound")
.font(.headline.weight(.semibold))
.foregroundStyle(.white)
}
@ -83,11 +79,7 @@ struct SoundControlView: View {
.animation(.easeInOut(duration: 0.2), value: isPlaying)
.animation(.easeInOut(duration: 0.2), value: selectedSound)
.accessibilityIdentifier("noise.playStopButton")
.accessibilityLabel(
isPlaying
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
)
.accessibilityLabel(isPlaying ? "Stop Sound" : "Play Sound")
}
.frame(maxWidth: 400) // Reasonable max width for iPad
.padding(Design.Spacing.medium)

View File

@ -44,10 +44,7 @@ struct NoiseView: View {
Image(systemName: "magnifyingglass")
.foregroundStyle(AppTextColors.secondary)
TextField(
String(localized: "noise.search.placeholder", defaultValue: "Search sounds"),
text: $searchText
)
TextField("Search sounds", text: $searchText)
.textFieldStyle(.plain)
.foregroundStyle(AppTextColors.primary)
.accessibilityIdentifier("noise.searchField")
@ -82,7 +79,7 @@ struct NoiseView: View {
.frame(maxWidth: .infinity, alignment: .center)
}
}
.navigationTitle(String(localized: "noise.navigation_title", defaultValue: "Noise"))
.navigationTitle("Noise")
.navigationBarTitleDisplayMode(.inline)
.animation(.easeInOut(duration: 0.3), value: selectedSound)
.accessibilityIdentifier("noise.screen")
@ -128,16 +125,11 @@ struct NoiseView: View {
}
VStack(spacing: 4) {
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
Text("Ready for Sleep?")
.typography(.title3Bold)
.foregroundStyle(AppTextColors.primary)
Text(
String(
localized: "noise.empty.subtitle_portrait",
defaultValue: "Select a soothing sound below to begin your relaxation journey."
)
)
Text("Select a soothing sound below to begin your relaxation journey.")
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
@ -178,7 +170,7 @@ struct NoiseView: View {
// Left side: Player controls
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
if !isPad {
Text(String(localized: "noise.ambient_sounds", defaultValue: "Ambient Sounds"))
Text("Ambient Sounds")
.sectionTitleStyle()
}
@ -199,11 +191,11 @@ struct NoiseView: View {
}
VStack(spacing: 4) {
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
Text("Ready for Sleep?")
.typography(.title3Bold)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "noise.empty.subtitle_landscape", defaultValue: "Select a soothing sound to begin."))
Text("Select a soothing sound to begin.")
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)

View File

@ -37,11 +37,7 @@ struct OnboardingBottomControls: View {
onSkip()
}
} label: {
Text(
currentPage == 0
? String(localized: "onboarding.controls.skip", defaultValue: "Skip")
: String(localized: "onboarding.controls.back", defaultValue: "Back")
)
Text(currentPage == 0 ? "Skip" : "Back")
.typography(.bodyEmphasis)
.foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity)
@ -56,11 +52,7 @@ struct OnboardingBottomControls: View {
onFinish()
}
} label: {
Text(
currentPage == totalPages - 1
? String(localized: "onboarding.controls.get_started", defaultValue: "Get Started")
: String(localized: "onboarding.controls.next", defaultValue: "Next")
)
Text(currentPage == totalPages - 1 ? "Get Started" : "Next")
.typography(.bodyEmphasis)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)

View File

@ -25,22 +25,22 @@ struct OnboardingGetStartedPage: View {
.foregroundStyle(AppStatus.success)
}
Text(String(localized: "onboarding.get_started.title", defaultValue: "You're ready!"))
Text("You're ready!")
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
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!"))
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!")
.typography(.body)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
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"))
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")
}
.padding(.top, Design.Spacing.medium)

View File

@ -33,23 +33,23 @@ struct OnboardingPermissionsPage: View {
.symbolEffect(.pulse, options: .repeating)
}
Text(String(localized: "onboarding.permissions.title", defaultValue: "Alarms that actually work"))
Text("Alarms that actually work")
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, Design.Spacing.large)
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."))
Text("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: 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"))
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")
}
.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(String(localized: "onboarding.permissions.enabled", defaultValue: "Alarms enabled!"))
Text("Alarms enabled!")
}
.foregroundStyle(AppStatus.success)
.typography(.bodyEmphasis)
@ -89,7 +89,7 @@ struct OnboardingPermissionsPage: View {
} label: {
HStack {
Image(systemName: "alarm.fill")
Text(String(localized: "onboarding.permissions.enable_alarms", defaultValue: "Enable Alarms"))
Text("Enable Alarms")
}
.typography(.bodyEmphasis)
.foregroundStyle(.white)
@ -106,7 +106,7 @@ struct OnboardingPermissionsPage: View {
private var keepAwakeSection: some View {
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "onboarding.permissions.keep_awake_prompt", defaultValue: "Want the clock always visible?"))
Text("Want the clock always visible?")
.typography(.callout)
.foregroundStyle(AppTextColors.tertiary)
.multilineTextAlignment(.center)
@ -116,11 +116,7 @@ struct OnboardingPermissionsPage: View {
} label: {
HStack(spacing: Design.Spacing.small) {
Image(systemName: keepAwakeEnabled ? "checkmark.circle.fill" : "bolt.fill")
Text(
keepAwakeEnabled
? String(localized: "onboarding.permissions.keep_awake_enabled", defaultValue: "Keep Awake Enabled")
: String(localized: "onboarding.permissions.enable_keep_awake", defaultValue: "Enable Keep Awake")
)
Text(keepAwakeEnabled ? "Keep Awake Enabled" : "Enable Keep Awake")
}
.typography(.callout)
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)

View File

@ -20,26 +20,26 @@ struct OnboardingWelcomePage: View {
}
.padding(.bottom, Design.Spacing.medium)
Text(String(localized: "onboarding.welcome.title", defaultValue: "The Noise Clock"))
Text("The Noise Clock")
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "onboarding.welcome.subtitle", defaultValue: "Your beautiful bedside companion"))
Text("Your beautiful bedside companion")
.typography(.title3)
.foregroundStyle(AppTextColors.secondary)
VStack(spacing: Design.Spacing.medium) {
OnboardingFeatureRow(
icon: "moon.stars.fill",
text: String(localized: "onboarding.welcome.feature_sleep_sounds", defaultValue: "Fall asleep to soothing sounds")
text: "Fall asleep to soothing sounds"
)
OnboardingFeatureRow(
icon: "alarm.fill",
text: String(localized: "onboarding.welcome.feature_custom_alarms", defaultValue: "Wake up your way with custom alarms")
text: "Wake up your way with custom alarms"
)
OnboardingFeatureRow(
icon: "clock.fill",
text: String(localized: "onboarding.welcome.feature_full_screen", defaultValue: "Automatic full-screen display")
text: "Automatic full-screen display"
)
}
.padding(.top, Design.Spacing.large)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
//
// 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 }
)
}
}

View File

@ -16,14 +16,10 @@ public enum DigitAnimationStyle: String, CaseIterable, Codable {
public var displayName: String {
switch self {
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")
case .none: return "None"
case .spring: return "Spring"
case .bounce: return "Bounce"
case .glitch: return "Glitch"
}
}
}

View File

@ -21,18 +21,12 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
/// Display name for the category
public var displayName: String {
switch self {
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")
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"
}
}
@ -63,18 +57,12 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
/// Description of the category
public var description: String {
switch self {
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")
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"
}
}

View File

@ -20,12 +20,12 @@ struct KeepAwakePrompt: View {
.symbolEffect(.bounce, options: .nonRepeating)
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "keep_awake.prompt.title", defaultValue: "Keep Awake for Alarms"))
Text("Keep Awake for Alarms")
.typography(.title2Bold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
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."))
Text("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(String(localized: "keep_awake.prompt.enable", defaultValue: "Enable Keep Awake"))
Text("Enable Keep Awake")
.font(Typography.headingEmphasis.font)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
@ -44,7 +44,7 @@ struct KeepAwakePrompt: View {
}
Button(action: onDismiss) {
Text(String(localized: "common.not_now", defaultValue: "Not Now"))
Text("Not Now")
.font(Typography.bodyEmphasis.font)
.foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity)