localization
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
3b45fe2114
commit
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
|
- **Async/Await**: Modern concurrency patterns throughout the codebase
|
||||||
- **Observation Framework**: @Observable for reactive state management
|
- **Observation Framework**: @Observable for reactive state management
|
||||||
- **SwiftUI Navigation**: Latest NavigationStack and navigation APIs with iOS 26 features
|
- **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
|
- **Accessibility**: Full VoiceOver and Dynamic Type support with iOS 26 enhancements
|
||||||
- **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass
|
- **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass
|
||||||
- **Performance**: Optimized for 120Hz ProMotion displays and iOS 26 performance improvements
|
- **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.
|
- 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.
|
- 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.
|
- 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 {
|
private var mainTabView: some View {
|
||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
Tab("Clock", systemImage: "clock", value: AppTab.clock) {
|
Tab(String(localized: "tab.clock", defaultValue: "Clock"), systemImage: "clock", value: AppTab.clock) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab)
|
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 {
|
NavigationStack {
|
||||||
AlarmView(viewModel: alarmViewModel)
|
AlarmView(viewModel: alarmViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab("Noise", systemImage: "waveform", value: AppTab.noise) {
|
Tab(String(localized: "tab.noise", defaultValue: "Noise"), systemImage: "waveform", value: AppTab.noise) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
NoiseView()
|
NoiseView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab("Settings", systemImage: "gearshape", value: AppTab.settings) {
|
Tab(String(localized: "tab.settings", defaultValue: "Settings"), systemImage: "gearshape", value: AppTab.settings) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ClockSettingsView(
|
ClockSettingsView(
|
||||||
style: clockViewModel.style,
|
style: clockViewModel.style,
|
||||||
|
|||||||
@ -17,10 +17,15 @@ import Foundation
|
|||||||
/// Intent to stop an active alarm from the Live Activity or notification.
|
/// Intent to stop an active alarm from the Live Activity or notification.
|
||||||
struct StopAlarmIntent: LiveActivityIntent {
|
struct StopAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static let title: LocalizedStringResource = "Stop Alarm"
|
static let title = LocalizedStringResource("alarm_intent.stop.title", defaultValue: "Stop Alarm")
|
||||||
static let description = IntentDescription("Stops the currently ringing 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
|
var alarmId: String
|
||||||
|
|
||||||
static var supportedModes: IntentModes { .background }
|
static var supportedModes: IntentModes { .background }
|
||||||
@ -48,10 +53,15 @@ struct StopAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to snooze an active alarm from the Live Activity or notification.
|
/// Intent to snooze an active alarm from the Live Activity or notification.
|
||||||
struct SnoozeAlarmIntent: LiveActivityIntent {
|
struct SnoozeAlarmIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static let title: LocalizedStringResource = "Snooze Alarm"
|
static let title = LocalizedStringResource("alarm_intent.snooze.title", defaultValue: "Snooze Alarm")
|
||||||
static let description = IntentDescription("Snoozes the currently ringing 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
|
var alarmId: String
|
||||||
|
|
||||||
static var supportedModes: IntentModes { .background }
|
static var supportedModes: IntentModes { .background }
|
||||||
@ -80,11 +90,16 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
|
|||||||
/// Intent to open the app when the user taps the Live Activity.
|
/// Intent to open the app when the user taps the Live Activity.
|
||||||
struct OpenAlarmAppIntent: LiveActivityIntent {
|
struct OpenAlarmAppIntent: LiveActivityIntent {
|
||||||
|
|
||||||
static let title: LocalizedStringResource = "Open TheNoiseClock"
|
static let title = LocalizedStringResource("alarm_intent.open_app.title", defaultValue: "Open TheNoiseClock")
|
||||||
static let description = IntentDescription("Opens the app to the alarm screen")
|
static let description = IntentDescription(
|
||||||
|
LocalizedStringResource(
|
||||||
|
"alarm_intent.open_app.description",
|
||||||
|
defaultValue: "Opens the app to the alarm screen"
|
||||||
|
)
|
||||||
|
)
|
||||||
static let openAppWhenRun = true
|
static let openAppWhenRun = true
|
||||||
|
|
||||||
@Parameter(title: "Alarm ID")
|
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
|
||||||
var alarmId: String
|
var alarmId: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -110,9 +125,9 @@ enum AlarmIntentError: Error, LocalizedError {
|
|||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidAlarmID:
|
case .invalidAlarmID:
|
||||||
return "Invalid alarm ID"
|
return String(localized: "alarm_intent.error.invalid_alarm_id", defaultValue: "Invalid alarm ID")
|
||||||
case .alarmNotFound:
|
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,
|
isEnabled: Bool = true,
|
||||||
repeatWeekdays: [Int] = [],
|
repeatWeekdays: [Int] = [],
|
||||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||||
label: String = "Alarm",
|
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
|
||||||
notificationMessage: String = "Your alarm is ringing",
|
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
|
||||||
snoozeDuration: Int = 9,
|
snoozeDuration: Int = 9,
|
||||||
isVibrationEnabled: Bool = true,
|
isVibrationEnabled: Bool = true,
|
||||||
isLightFlashEnabled: Bool = false,
|
isLightFlashEnabled: Bool = false,
|
||||||
@ -72,8 +72,8 @@ struct Alarm: Identifiable, Codable, Equatable {
|
|||||||
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
|
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
|
||||||
)
|
)
|
||||||
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
|
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
|
||||||
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm"
|
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) ?? "Your alarm is ringing"
|
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.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9
|
||||||
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
|
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
|
||||||
self.isLightFlashEnabled = try container.decodeIfPresent(Bool.self, forKey: .isLightFlashEnabled) ?? false
|
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 {
|
static func repeatSummary(for weekdays: [Int]) -> String {
|
||||||
let normalized = sanitizedWeekdays(weekdays)
|
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)
|
let weekdaySet = Set(normalized)
|
||||||
if weekdaySet.count == 7 {
|
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]) {
|
if weekdaySet == Set([2, 3, 4, 5, 6]) {
|
||||||
return "Weekdays"
|
return String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays")
|
||||||
}
|
}
|
||||||
|
|
||||||
if weekdaySet == Set([1, 7]) {
|
if weekdaySet == Set([1, 7]) {
|
||||||
return "Weekends"
|
return String(localized: "alarm.repeat.weekends", defaultValue: "Weekends")
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbols = Calendar.current.shortWeekdaySymbols
|
let symbols = Calendar.current.shortWeekdaySymbols
|
||||||
|
|||||||
@ -83,14 +83,14 @@ final class AlarmKitService {
|
|||||||
|
|
||||||
// Create the stop button for the alarm
|
// Create the stop button for the alarm
|
||||||
let stopButton = AlarmButton(
|
let stopButton = AlarmButton(
|
||||||
text: "Stop",
|
text: LocalizedStringResource("alarm.action.stop"),
|
||||||
textColor: .red,
|
textColor: .red,
|
||||||
systemImageName: "stop.fill"
|
systemImageName: "stop.fill"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create the snooze button (secondary button with countdown behavior)
|
// Create the snooze button (secondary button with countdown behavior)
|
||||||
let snoozeButton = AlarmButton(
|
let snoozeButton = AlarmButton(
|
||||||
text: "Snooze",
|
text: LocalizedStringResource("alarm.action.snooze"),
|
||||||
textColor: .white,
|
textColor: .white,
|
||||||
systemImageName: "moon.zzz"
|
systemImageName: "moon.zzz"
|
||||||
)
|
)
|
||||||
@ -422,9 +422,15 @@ enum AlarmKitError: Error, LocalizedError {
|
|||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .notAuthorized:
|
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):
|
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,
|
time: Date,
|
||||||
repeatWeekdays: [Int] = [],
|
repeatWeekdays: [Int] = [],
|
||||||
soundName: String = AppConstants.SystemSounds.defaultSound,
|
soundName: String = AppConstants.SystemSounds.defaultSound,
|
||||||
label: String = "Alarm",
|
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
|
||||||
notificationMessage: String = "Your alarm is ringing",
|
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
|
||||||
snoozeDuration: Int = 9,
|
snoozeDuration: Int = 9,
|
||||||
isVibrationEnabled: Bool = true,
|
isVibrationEnabled: Bool = true,
|
||||||
isLightFlashEnabled: Bool = false,
|
isLightFlashEnabled: Bool = false,
|
||||||
@ -219,6 +219,9 @@ final class AlarmViewModel {
|
|||||||
} else {
|
} else {
|
||||||
error.localizedDescription
|
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 newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date()
|
||||||
@State private var repeatWeekdays: [Int] = []
|
@State private var repeatWeekdays: [Int] = []
|
||||||
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
|
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
|
||||||
@State private var alarmLabel = "Alarm"
|
@State private var alarmLabel = String(localized: "alarm.default_label", defaultValue: "Alarm")
|
||||||
@State private var notificationMessage = "Your alarm is ringing"
|
@State private var notificationMessage = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
|
||||||
@State private var snoozeDuration = 9 // minutes
|
@State private var snoozeDuration = 9 // minutes
|
||||||
@State private var isVibrationEnabled = true
|
@State private var isVibrationEnabled = true
|
||||||
@State private var isLightFlashEnabled = false
|
@State private var isLightFlashEnabled = false
|
||||||
@ -44,7 +44,7 @@ struct AddAlarmView: View {
|
|||||||
Image(systemName: "textformat")
|
Image(systemName: "textformat")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Label")
|
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(alarmLabel)
|
Text(alarmLabel)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@ -57,7 +57,7 @@ struct AddAlarmView: View {
|
|||||||
Image(systemName: "message")
|
Image(systemName: "message")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Message")
|
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(notificationMessage)
|
Text(notificationMessage)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@ -71,7 +71,7 @@ struct AddAlarmView: View {
|
|||||||
Image(systemName: "music.note")
|
Image(systemName: "music.note")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Sound")
|
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(getSoundDisplayName(selectedSoundName))
|
Text(getSoundDisplayName(selectedSoundName))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@ -84,7 +84,7 @@ struct AddAlarmView: View {
|
|||||||
Image(systemName: "repeat")
|
Image(systemName: "repeat")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Repeat")
|
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@ -98,9 +98,14 @@ struct AddAlarmView: View {
|
|||||||
Image(systemName: "clock.arrow.circlepath")
|
Image(systemName: "clock.arrow.circlepath")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Snooze")
|
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("for \(snoozeDuration) min")
|
Text(
|
||||||
|
String(
|
||||||
|
localized: "alarm.editor.snooze_for_minutes",
|
||||||
|
defaultValue: "for \(snoozeDuration) min"
|
||||||
|
)
|
||||||
|
)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,12 +118,12 @@ struct AddAlarmView: View {
|
|||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
}
|
}
|
||||||
.navigationTitle("Alarm")
|
.navigationTitle(String(localized: "alarm.add.navigation_title", defaultValue: "Alarm"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
@ -126,7 +131,11 @@ struct AddAlarmView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(isSaving ? "Saving..." : "Save") {
|
Button(
|
||||||
|
isSaving
|
||||||
|
? String(localized: "common.saving", defaultValue: "Saving...")
|
||||||
|
: String(localized: "common.save", defaultValue: "Save")
|
||||||
|
) {
|
||||||
Task {
|
Task {
|
||||||
await saveAlarm()
|
await saveAlarm()
|
||||||
}
|
}
|
||||||
@ -138,8 +147,8 @@ struct AddAlarmView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
|
||||||
Button("OK", role: .cancel) { }
|
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
|
||||||
} message: {
|
} message: {
|
||||||
Text(saveErrorMessage)
|
Text(saveErrorMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ struct AlarmView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(isPad ? "" : "Alarms")
|
.navigationTitle(isPad ? "" : String(localized: "alarms.navigation_title", defaultValue: "Alarms"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
@ -107,8 +107,8 @@ struct AlarmView: View {
|
|||||||
}
|
}
|
||||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
|
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
|
||||||
.accessibilityIdentifier("alarms.screen")
|
.accessibilityIdentifier("alarms.screen")
|
||||||
.alert("Alarm Error", isPresented: $viewModel.isShowingErrorAlert) {
|
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $viewModel.isShowingErrorAlert) {
|
||||||
Button("OK", role: .cancel) {
|
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {
|
||||||
viewModel.dismissErrorAlert()
|
viewModel.dismissErrorAlert()
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
|
|||||||
@ -36,7 +36,12 @@ struct AlarmRowView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
|
||||||
Text("• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
|
Text(
|
||||||
|
String(
|
||||||
|
localized: "alarm.row.sound_prefix",
|
||||||
|
defaultValue: "• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
|
||||||
|
)
|
||||||
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|
||||||
@ -46,7 +51,7 @@ struct AlarmRowView: View {
|
|||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(AppStatus.warning)
|
.foregroundStyle(AppStatus.warning)
|
||||||
.symbolEffect(.pulse, options: .repeating)
|
.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)
|
.font(.caption2)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
@ -73,14 +78,23 @@ struct AlarmRowView: View {
|
|||||||
}
|
}
|
||||||
.accessibilityElement(children: .contain)
|
.accessibilityElement(children: .contain)
|
||||||
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
|
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
|
||||||
.accessibilityLabel("\(alarm.label), \(alarm.formattedTime())")
|
.accessibilityLabel(
|
||||||
.accessibilityValue(alarm.isEnabled ? "Enabled" : "Disabled")
|
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) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
onDelete()
|
onDelete()
|
||||||
} label: {
|
} 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
|
@Binding var volume: Float
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section("Alert Options") {
|
Section(String(localized: "alarm.alert_options.section_title", defaultValue: "Alert Options")) {
|
||||||
Toggle("Vibration", isOn: $isVibrationEnabled)
|
Toggle(String(localized: "alarm.alert_options.vibration", defaultValue: "Vibration"), isOn: $isVibrationEnabled)
|
||||||
.tint(AppAccent.primary)
|
.tint(AppAccent.primary)
|
||||||
.accessibilityIdentifier("alarms.alertOptions.vibrationToggle")
|
.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)
|
.tint(AppAccent.primary)
|
||||||
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
|
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Volume")
|
Text(String(localized: "alarm.alert_options.volume", defaultValue: "Volume"))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(Int((volume * 100).rounded()))%")
|
Text(Double(volume), format: .percent.precision(.fractionLength(0)))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,11 +32,11 @@ struct EmptyAlarmsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
Text("No Alarms Set")
|
Text(String(localized: "alarms.empty.title", defaultValue: "No Alarms Set"))
|
||||||
.typography(.title2Bold)
|
.typography(.title2Bold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.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)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -48,7 +48,7 @@ struct EmptyAlarmsView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
.symbolEffect(.bounce, options: .nonRepeating)
|
.symbolEffect(.bounce, options: .nonRepeating)
|
||||||
Text("Add Your First Alarm")
|
Text(String(localized: "alarms.empty.cta", defaultValue: "Add Your First Alarm"))
|
||||||
}
|
}
|
||||||
.typography(.bodyEmphasis)
|
.typography(.bodyEmphasis)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|||||||
@ -16,12 +16,15 @@ struct LabelEditView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
TextField("Alarm Label", text: $label)
|
TextField(
|
||||||
|
String(localized: "alarm.label.placeholder", defaultValue: "Alarm Label"),
|
||||||
|
text: $label
|
||||||
|
)
|
||||||
.typography(.body)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Enter a name for your alarm.")
|
Text(String(localized: "alarm.label.footer", defaultValue: "Enter a name for your alarm."))
|
||||||
.typography(.caption)
|
.typography(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
}
|
}
|
||||||
@ -29,7 +32,7 @@ struct LabelEditView: View {
|
|||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
.navigationTitle("Label")
|
.navigationTitle(String(localized: "alarm.label.navigation_title", defaultValue: "Label"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ struct NotificationMessageEditView: View {
|
|||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.padding(.vertical, Design.Spacing.xxSmall)
|
.padding(.vertical, Design.Spacing.xxSmall)
|
||||||
} footer: {
|
} 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)
|
.typography(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ struct NotificationMessageEditView: View {
|
|||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
.navigationTitle("Message")
|
.navigationTitle(String(localized: "alarm.message.navigation_title", defaultValue: "Message"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,14 +16,14 @@ struct RepeatSelectionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section("Quick Picks") {
|
Section(String(localized: "alarm.repeat.quick_picks_section", defaultValue: "Quick Picks")) {
|
||||||
quickPickRow(id: "once", title: "Once", weekdays: [])
|
quickPickRow(id: "once", title: String(localized: "alarm.repeat.once", defaultValue: "Once"), weekdays: [])
|
||||||
quickPickRow(id: "everyday", title: "Every Day", weekdays: allWeekdays)
|
quickPickRow(id: "everyday", title: String(localized: "alarm.repeat.every_day", defaultValue: "Every Day"), weekdays: allWeekdays)
|
||||||
quickPickRow(id: "weekdays", title: "Weekdays", weekdays: [2, 3, 4, 5, 6])
|
quickPickRow(id: "weekdays", title: String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays"), weekdays: [2, 3, 4, 5, 6])
|
||||||
quickPickRow(id: "weekends", title: "Weekends", weekdays: [1, 7])
|
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
|
ForEach(orderedWeekdays, id: \.self) { weekday in
|
||||||
HStack {
|
HStack {
|
||||||
Text(dayName(for: weekday))
|
Text(dayName(for: weekday))
|
||||||
@ -46,7 +46,7 @@ struct RepeatSelectionView: View {
|
|||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
.navigationTitle("Repeat")
|
.navigationTitle(String(localized: "alarm.repeat.navigation_title", defaultValue: "Repeat"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,15 @@ struct SnoozeSelectionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section("Snooze Duration") {
|
Section(String(localized: "alarm.snooze.duration_section", defaultValue: "Snooze Duration")) {
|
||||||
ForEach(snoozeOptions, id: \.self) { duration in
|
ForEach(snoozeOptions, id: \.self) { duration in
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(duration) minutes")
|
Text(
|
||||||
|
String(
|
||||||
|
localized: "alarm.snooze.duration_minutes",
|
||||||
|
defaultValue: "\(duration) minutes"
|
||||||
|
)
|
||||||
|
)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
if snoozeDuration == duration {
|
if snoozeDuration == duration {
|
||||||
@ -39,7 +44,7 @@ struct SnoozeSelectionView: View {
|
|||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
.navigationTitle("Snooze")
|
.navigationTitle(String(localized: "alarm.snooze.navigation_title", defaultValue: "Snooze"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ struct SoundSelectionView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section("Alarm Sounds") {
|
Section(String(localized: "alarm.sound.section_title", defaultValue: "Alarm Sounds")) {
|
||||||
ForEach(alarmSounds, id: \.id) { sound in
|
ForEach(alarmSounds, id: \.id) { sound in
|
||||||
HStack {
|
HStack {
|
||||||
Text(sound.name)
|
Text(sound.name)
|
||||||
@ -50,7 +50,7 @@ struct SoundSelectionView: View {
|
|||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
.navigationTitle("Sound")
|
.navigationTitle(String(localized: "alarm.sound.navigation_title", defaultValue: "Sound"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ struct TimePickerSection: View {
|
|||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DatePicker(
|
DatePicker(
|
||||||
"Time",
|
String(localized: "alarm.time_picker.label", defaultValue: "Time"),
|
||||||
selection: $selectedTime,
|
selection: $selectedTime,
|
||||||
displayedComponents: .hourAndMinute
|
displayedComponents: .hourAndMinute
|
||||||
)
|
)
|
||||||
|
|||||||
@ -46,23 +46,29 @@ struct TimeUntilAlarmSection: View {
|
|||||||
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
|
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
|
||||||
if let hours = components.hour, let minutes = components.minute {
|
if let hours = components.hour, let minutes = components.minute {
|
||||||
if hours > 0 {
|
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 {
|
} 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 {
|
} 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 {
|
private var dayText: String {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
if calendar.isDateInToday(nextAlarmTime) {
|
if calendar.isDateInToday(nextAlarmTime) {
|
||||||
return "Today"
|
return String(localized: "calendar.today", defaultValue: "Today")
|
||||||
} else if calendar.isDateInTomorrow(nextAlarmTime) {
|
} else if calendar.isDateInTomorrow(nextAlarmTime) {
|
||||||
return "Tomorrow"
|
return String(localized: "calendar.tomorrow", defaultValue: "Tomorrow")
|
||||||
} else {
|
} else {
|
||||||
return nextAlarmTime.formatted(.dateTime.weekday(.wide))
|
return nextAlarmTime.formatted(.dateTime.weekday(.wide))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,7 @@ struct EditAlarmView: View {
|
|||||||
Image(systemName: "textformat")
|
Image(systemName: "textformat")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Label")
|
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(alarmLabel)
|
Text(alarmLabel)
|
||||||
@ -79,7 +79,7 @@ struct EditAlarmView: View {
|
|||||||
Image(systemName: "message")
|
Image(systemName: "message")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Message")
|
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(notificationMessage)
|
Text(notificationMessage)
|
||||||
@ -95,7 +95,7 @@ struct EditAlarmView: View {
|
|||||||
Image(systemName: "music.note")
|
Image(systemName: "music.note")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Sound")
|
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(getSoundDisplayName(selectedSoundName))
|
Text(getSoundDisplayName(selectedSoundName))
|
||||||
@ -110,7 +110,7 @@ struct EditAlarmView: View {
|
|||||||
Image(systemName: "repeat")
|
Image(systemName: "repeat")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Repeat")
|
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
Text(Alarm.repeatSummary(for: repeatWeekdays))
|
||||||
@ -126,10 +126,15 @@ struct EditAlarmView: View {
|
|||||||
Image(systemName: "clock.arrow.circlepath")
|
Image(systemName: "clock.arrow.circlepath")
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
Text("Snooze")
|
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("for \(snoozeDuration) min")
|
Text(
|
||||||
|
String(
|
||||||
|
localized: "alarm.editor.snooze_for_minutes",
|
||||||
|
defaultValue: "for \(snoozeDuration) min"
|
||||||
|
)
|
||||||
|
)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,12 +151,12 @@ struct EditAlarmView: View {
|
|||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppSurface.primary.ignoresSafeArea())
|
.background(AppSurface.primary.ignoresSafeArea())
|
||||||
}
|
}
|
||||||
.navigationTitle("Edit Alarm")
|
.navigationTitle(String(localized: "alarm.edit.navigation_title", defaultValue: "Edit Alarm"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel") {
|
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.foregroundStyle(AppAccent.primary)
|
.foregroundStyle(AppAccent.primary)
|
||||||
@ -159,7 +164,11 @@ struct EditAlarmView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(isSaving ? "Saving..." : "Save") {
|
Button(
|
||||||
|
isSaving
|
||||||
|
? String(localized: "common.saving", defaultValue: "Saving...")
|
||||||
|
: String(localized: "common.save", defaultValue: "Save")
|
||||||
|
) {
|
||||||
Task {
|
Task {
|
||||||
await saveAlarm()
|
await saveAlarm()
|
||||||
}
|
}
|
||||||
@ -171,8 +180,8 @@ struct EditAlarmView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
|
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
|
||||||
Button("OK", role: .cancel) { }
|
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
|
||||||
} message: {
|
} message: {
|
||||||
Text(saveErrorMessage)
|
Text(saveErrorMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,23 +55,23 @@ struct ClockSettingsView: View {
|
|||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Debug",
|
title: String(localized: "settings.debug.section_title", defaultValue: "Debug"),
|
||||||
systemImage: "ant.fill",
|
systemImage: "ant.fill",
|
||||||
accentColor: AppStatus.error
|
accentColor: AppStatus.error
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Icon Generator",
|
title: String(localized: "settings.debug.icon_generator.title", defaultValue: "Icon Generator"),
|
||||||
subtitle: "Generate and save app icon",
|
subtitle: String(localized: "settings.debug.icon_generator.subtitle", defaultValue: "Generate and save app icon"),
|
||||||
backgroundColor: AppSurface.primary
|
backgroundColor: AppSurface.primary
|
||||||
) {
|
) {
|
||||||
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
|
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Branding Preview",
|
title: String(localized: "settings.debug.branding_preview.title", defaultValue: "Branding Preview"),
|
||||||
subtitle: "Preview icon and launch screen",
|
subtitle: String(localized: "settings.debug.branding_preview.subtitle", defaultValue: "Preview icon and launch screen"),
|
||||||
backgroundColor: AppSurface.primary
|
backgroundColor: AppSurface.primary
|
||||||
) {
|
) {
|
||||||
BrandingPreviewView(
|
BrandingPreviewView(
|
||||||
@ -90,10 +90,10 @@ struct ClockSettingsView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
Text("Reset Onboarding")
|
Text(String(localized: "settings.debug.reset_onboarding.title", defaultValue: "Reset Onboarding"))
|
||||||
.typography(.body)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.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)
|
.typography(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ struct ClockSettingsView: View {
|
|||||||
.padding(.bottom, Design.Spacing.xxxLarge)
|
.padding(.bottom, Design.Spacing.xxxLarge)
|
||||||
}
|
}
|
||||||
.background(AppSurface.primary)
|
.background(AppSurface.primary)
|
||||||
.navigationTitle(isPad ? "" : "Settings")
|
.navigationTitle(isPad ? "" : String(localized: "settings.navigation_title", defaultValue: "Settings"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
digitColor = Color(hex: style.digitColorHex) ?? .white
|
digitColor = Color(hex: style.digitColorHex) ?? .white
|
||||||
|
|||||||
@ -180,11 +180,26 @@ struct ClockView: View {
|
|||||||
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
|
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Screen: \(Int(size.width))×\(Int(size.height))")
|
Text(String(localized: "clock.debug.screen", defaultValue: "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(
|
||||||
Text("Symmetric Inset: \(Int(symmetricInset))")
|
String(
|
||||||
Text("Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")")
|
localized: "clock.debug.window_insets",
|
||||||
Text("Orientation: \(isLandscape ? "Landscape" : "Portrait")")
|
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))
|
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ struct BatteryOverlayView: View {
|
|||||||
.foregroundStyle(batteryColor)
|
.foregroundStyle(batteryColor)
|
||||||
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
|
.symbolEffect(.pulse, options: .repeating, isActive: isCharging)
|
||||||
.contentTransition(.symbolEffect(.replace))
|
.contentTransition(.symbolEffect(.replace))
|
||||||
Text("\(batteryLevel)%")
|
Text(Double(batteryLevel) / 100, format: .percent.precision(.fractionLength(0)))
|
||||||
.foregroundStyle(color)
|
.foregroundStyle(color)
|
||||||
.contentTransition(.numericText())
|
.contentTransition(.numericText())
|
||||||
.animation(.snappy(duration: 0.3), value: batteryLevel)
|
.animation(.snappy(duration: 0.3), value: batteryLevel)
|
||||||
|
|||||||
@ -45,7 +45,7 @@ struct ClockDisplayContainer: View {
|
|||||||
.animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode)
|
.animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityIdentifier("clock.timeDisplay")
|
.accessibilityIdentifier("clock.timeDisplay")
|
||||||
.accessibilityLabel("Current time")
|
.accessibilityLabel(String(localized: "clock.accessibility.current_time", defaultValue: "Current time"))
|
||||||
.accessibilityValue(accessibilityTimeValue)
|
.accessibilityValue(accessibilityTimeValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ struct ClockToolbar: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
let isPad = UIDevice.current.userInterfaceIdiom == .pad
|
let isPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||||
EmptyView()
|
EmptyView()
|
||||||
.navigationTitle(isDisplayMode || isPad ? "" : "Clock")
|
.navigationTitle(isDisplayMode || isPad ? "" : String(localized: "clock.navigation_title", defaultValue: "Clock"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarBackButtonHidden(isDisplayMode)
|
.navigationBarBackButtonHidden(isDisplayMode)
|
||||||
.toolbar(isDisplayMode ? .hidden : .automatic)
|
.toolbar(isDisplayMode ? .hidden : .automatic)
|
||||||
|
|||||||
@ -195,7 +195,7 @@ private struct GlowAnimationModifier: ViewModifier {
|
|||||||
animationStyle: .spring)
|
animationStyle: .spring)
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
|
|
||||||
Text(":")
|
Text(String(localized: "clock.time.colon", defaultValue: ":"))
|
||||||
.font(.system(size: sharedFontSize))
|
.font(.system(size: sharedFontSize))
|
||||||
.border(Color.black)
|
.border(Color.black)
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,11 @@ struct NoiseMiniPlayer: View {
|
|||||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
|
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
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))
|
.font(.system(size: 8, weight: .bold))
|
||||||
.foregroundStyle(color.opacity(0.6))
|
.foregroundStyle(color.opacity(0.6))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ struct AdvancedAppearanceSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Advanced Appearance",
|
title: String(localized: "settings.advanced_appearance.section_title", defaultValue: "Advanced Appearance"),
|
||||||
systemImage: "sparkles",
|
systemImage: "sparkles",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -22,14 +22,14 @@ struct AdvancedAppearanceSection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Digit Animation",
|
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
|
||||||
subtitle: style.digitAnimationStyle.displayName,
|
subtitle: style.digitAnimationStyle.displayName,
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.digitAnimationStyle,
|
selection: $style.digitAnimationStyle,
|
||||||
options: DigitAnimationStyle.allCases,
|
options: DigitAnimationStyle.allCases,
|
||||||
title: "Digit Animation",
|
title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
|
||||||
toString: { $0.displayName }
|
toString: { $0.displayName }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -40,8 +40,8 @@ struct AdvancedAppearanceSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Randomize Color",
|
title: String(localized: "settings.advanced_appearance.randomize_color.title", defaultValue: "Randomize Color"),
|
||||||
subtitle: "Shift the color every minute",
|
subtitle: String(localized: "settings.advanced_appearance.randomize_color.subtitle", defaultValue: "Shift the color every minute"),
|
||||||
isOn: $style.randomizeColor,
|
isOn: $style.randomizeColor,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -52,8 +52,8 @@ struct AdvancedAppearanceSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsSlider(
|
SettingsSlider(
|
||||||
title: "Glow",
|
title: String(localized: "settings.advanced_appearance.glow.title", defaultValue: "Glow"),
|
||||||
subtitle: "Adjust the glow intensity",
|
subtitle: String(localized: "settings.advanced_appearance.glow.subtitle", defaultValue: "Adjust the glow intensity"),
|
||||||
value: $style.glowIntensity,
|
value: $style.glowIntensity,
|
||||||
in: 0.0...1.0,
|
in: 0.0...1.0,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
@ -67,8 +67,8 @@ struct AdvancedAppearanceSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsSlider(
|
SettingsSlider(
|
||||||
title: "Clock Opacity",
|
title: String(localized: "settings.advanced_appearance.clock_opacity.title", defaultValue: "Clock Opacity"),
|
||||||
subtitle: "Set the clock transparency",
|
subtitle: String(localized: "settings.advanced_appearance.clock_opacity.subtitle", defaultValue: "Set the clock transparency"),
|
||||||
value: $style.clockOpacity,
|
value: $style.clockOpacity,
|
||||||
in: 0.0...1.0,
|
in: 0.0...1.0,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
@ -78,7 +78,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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ struct AdvancedDisplaySection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Advanced Display",
|
title: String(localized: "settings.advanced_display.section_title", defaultValue: "Advanced Display"),
|
||||||
systemImage: "eye",
|
systemImage: "eye",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -22,8 +22,8 @@ struct AdvancedDisplaySection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Keep Awake",
|
title: String(localized: "settings.advanced_display.keep_awake.title", defaultValue: "Keep Awake"),
|
||||||
subtitle: "Prevent sleep in display mode",
|
subtitle: String(localized: "settings.advanced_display.keep_awake.subtitle", defaultValue: "Prevent sleep in display mode"),
|
||||||
isOn: $style.keepAwake,
|
isOn: $style.keepAwake,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -36,11 +36,11 @@ struct AdvancedDisplaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Current Brightness")
|
Text(String(localized: "settings.advanced_display.current_brightness", defaultValue: "Current Brightness"))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(Int(style.effectiveBrightness * 100))%")
|
Text(style.effectiveBrightness, format: .percent.precision(.fractionLength(0)))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.contentTransition(.numericText())
|
.contentTransition(.numericText())
|
||||||
@ -52,27 +52,27 @@ struct AdvancedDisplaySection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Focus Modes",
|
title: String(localized: "settings.focus_modes.section_title", defaultValue: "Focus Modes"),
|
||||||
systemImage: "moon.zzz.fill",
|
systemImage: "moon.zzz.fill",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Respect Focus Modes",
|
title: String(localized: "settings.focus_modes.respect_focus.title", defaultValue: "Respect Focus Modes"),
|
||||||
subtitle: "Follow Do Not Disturb rules",
|
subtitle: String(localized: "settings.focus_modes.respect_focus.subtitle", defaultValue: "Follow Do Not Disturb rules"),
|
||||||
isOn: $style.respectFocusModes,
|
isOn: $style.respectFocusModes,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
.accessibilityIdentifier("settings.respectFocus.toggle")
|
.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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ struct BasicAppearanceSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Colors",
|
title: String(localized: "settings.colors.section_title", defaultValue: "Colors"),
|
||||||
systemImage: "paintpalette.fill",
|
systemImage: "paintpalette.fill",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -24,16 +24,16 @@ struct BasicAppearanceSection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Color Theme",
|
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
|
||||||
subtitle: style.selectedColorTheme,
|
subtitle: localizedThemeName(for: style.selectedColorTheme),
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.selectedColorTheme,
|
selection: $style.selectedColorTheme,
|
||||||
options: ClockStyle.availableColorThemes().map { $0.0 },
|
options: ClockStyle.availableColorThemes().map { $0.0 },
|
||||||
title: "Color Theme",
|
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
|
||||||
toString: { theme in
|
toString: { theme in
|
||||||
ClockStyle.availableColorThemes().first(where: { $0.0 == theme })?.1 ?? theme
|
localizedThemeName(for: theme)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -51,7 +51,11 @@ struct BasicAppearanceSection: View {
|
|||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
|
ColorPicker(
|
||||||
|
String(localized: "settings.colors.digit_color", defaultValue: "Digit Color"),
|
||||||
|
selection: $digitColor,
|
||||||
|
supportsOpacity: false
|
||||||
|
)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
@ -61,7 +65,11 @@ struct BasicAppearanceSection: View {
|
|||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
|
ColorPicker(
|
||||||
|
String(localized: "settings.colors.background_color", defaultValue: "Background Color"),
|
||||||
|
selection: $backgroundColor,
|
||||||
|
supportsOpacity: true
|
||||||
|
)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
@ -69,7 +77,7 @@ struct BasicAppearanceSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
@ -85,33 +93,32 @@ struct BasicAppearanceSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the color for a theme
|
private func localizedThemeName(for theme: String) -> String {
|
||||||
private func themeColor(for theme: String) -> Color {
|
|
||||||
switch theme {
|
switch theme {
|
||||||
case "Custom":
|
case "Custom":
|
||||||
return .gray
|
return String(localized: "settings.colors.theme.custom", defaultValue: "Custom")
|
||||||
case "Night":
|
case "Night":
|
||||||
return .white
|
return String(localized: "settings.colors.theme.night", defaultValue: "Night")
|
||||||
case "Day":
|
case "Day":
|
||||||
return .black
|
return String(localized: "settings.colors.theme.day", defaultValue: "Day")
|
||||||
case "Red":
|
case "Red":
|
||||||
return .red
|
return String(localized: "settings.colors.theme.red", defaultValue: "Red")
|
||||||
case "Orange":
|
case "Orange":
|
||||||
return .orange
|
return String(localized: "settings.colors.theme.orange", defaultValue: "Orange")
|
||||||
case "Yellow":
|
case "Yellow":
|
||||||
return .yellow
|
return String(localized: "settings.colors.theme.yellow", defaultValue: "Yellow")
|
||||||
case "Green":
|
case "Green":
|
||||||
return .green
|
return String(localized: "settings.colors.theme.green", defaultValue: "Green")
|
||||||
case "Blue":
|
case "Blue":
|
||||||
return .blue
|
return String(localized: "settings.colors.theme.blue", defaultValue: "Blue")
|
||||||
case "Purple":
|
case "Purple":
|
||||||
return .purple
|
return String(localized: "settings.colors.theme.purple", defaultValue: "Purple")
|
||||||
case "Pink":
|
case "Pink":
|
||||||
return .pink
|
return String(localized: "settings.colors.theme.pink", defaultValue: "Pink")
|
||||||
case "White":
|
case "White":
|
||||||
return .white
|
return String(localized: "settings.colors.theme.white", defaultValue: "White")
|
||||||
default:
|
default:
|
||||||
return .gray
|
return theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ struct BasicDisplaySection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Display",
|
title: String(localized: "settings.display.section_title", defaultValue: "Display"),
|
||||||
systemImage: "display",
|
systemImage: "display",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -22,8 +22,8 @@ struct BasicDisplaySection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "24‑Hour Format",
|
title: String(localized: "settings.display.use_24_hour.title", defaultValue: "24‑Hour Format"),
|
||||||
subtitle: "Use military time",
|
subtitle: String(localized: "settings.display.use_24_hour.subtitle", defaultValue: "Use military time"),
|
||||||
isOn: $style.use24Hour,
|
isOn: $style.use24Hour,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -34,8 +34,8 @@ struct BasicDisplaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Show Seconds",
|
title: String(localized: "settings.display.show_seconds.title", defaultValue: "Show Seconds"),
|
||||||
subtitle: "Display seconds in the clock",
|
subtitle: String(localized: "settings.display.show_seconds.subtitle", defaultValue: "Display seconds in the clock"),
|
||||||
isOn: $style.showSeconds,
|
isOn: $style.showSeconds,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -47,8 +47,8 @@ struct BasicDisplaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Show AM/PM",
|
title: String(localized: "settings.display.show_ampm.title", defaultValue: "Show AM/PM"),
|
||||||
subtitle: "Add an AM/PM indicator",
|
subtitle: String(localized: "settings.display.show_ampm.subtitle", defaultValue: "Add an AM/PM indicator"),
|
||||||
isOn: $style.showAmPm,
|
isOn: $style.showAmPm,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -60,8 +60,8 @@ struct BasicDisplaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Auto Brightness",
|
title: String(localized: "settings.display.auto_brightness.title", defaultValue: "Auto Brightness"),
|
||||||
subtitle: "Adapt brightness to ambient light",
|
subtitle: String(localized: "settings.display.auto_brightness.subtitle", defaultValue: "Adapt brightness to ambient light"),
|
||||||
isOn: $style.autoBrightness,
|
isOn: $style.autoBrightness,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -73,8 +73,8 @@ struct BasicDisplaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Horizontal Mode",
|
title: String(localized: "settings.display.horizontal_mode.title", defaultValue: "Horizontal Mode"),
|
||||||
subtitle: "Force a wide layout in portrait",
|
subtitle: String(localized: "settings.display.horizontal_mode.subtitle", defaultValue: "Force a wide layout in portrait"),
|
||||||
isOn: $style.forceHorizontalMode,
|
isOn: $style.forceHorizontalMode,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -82,7 +82,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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ struct FontSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Font",
|
title: String(localized: "settings.font.section_title", defaultValue: "Font"),
|
||||||
systemImage: "textformat",
|
systemImage: "textformat",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -40,14 +40,14 @@ struct FontSection: View {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Font Family
|
// Font Family
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Family",
|
title: String(localized: "settings.font.family.title", defaultValue: "Family"),
|
||||||
subtitle: style.fontFamily.rawValue,
|
subtitle: style.fontFamily.rawValue,
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.fontFamily,
|
selection: $style.fontFamily,
|
||||||
options: sortedFontFamilies,
|
options: sortedFontFamilies,
|
||||||
title: "Font Family",
|
title: String(localized: "settings.font.family.selection_title", defaultValue: "Font Family"),
|
||||||
toString: { $0.rawValue }
|
toString: { $0.rawValue }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -65,18 +65,18 @@ struct FontSection: View {
|
|||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(AppBorder.subtle)
|
.fill(AppBorder.subtle)
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.small)
|
||||||
|
|
||||||
// Font Weight
|
// Font Weight
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Weight",
|
title: String(localized: "settings.font.weight.title", defaultValue: "Weight"),
|
||||||
subtitle: style.fontWeight.rawValue,
|
subtitle: style.fontWeight.rawValue,
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.fontWeight,
|
selection: $style.fontWeight,
|
||||||
options: availableWeights,
|
options: availableWeights,
|
||||||
title: "Font Weight",
|
title: String(localized: "settings.font.weight.selection_title", defaultValue: "Font Weight"),
|
||||||
toString: { $0.rawValue }
|
toString: { $0.rawValue }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -89,14 +89,14 @@ struct FontSection: View {
|
|||||||
|
|
||||||
// Font Design
|
// Font Design
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Design",
|
title: String(localized: "settings.font.design.title", defaultValue: "Design"),
|
||||||
subtitle: style.fontDesign.rawValue,
|
subtitle: style.fontDesign.rawValue,
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.fontDesign,
|
selection: $style.fontDesign,
|
||||||
options: Font.Design.allCases,
|
options: Font.Design.allCases,
|
||||||
title: "Font Design",
|
title: String(localized: "settings.font.design.selection_title", defaultValue: "Font Design"),
|
||||||
toString: { $0.rawValue }
|
toString: { $0.rawValue }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -108,15 +108,14 @@ struct FontSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Preview")
|
Text(String(localized: "settings.font.preview.title", defaultValue: "Preview")).styled(.subheadingEmphasis)
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("12:34")
|
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))
|
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
.padding(.horizontal, Design.Spacing.small)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ struct NightModeSection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Night Mode",
|
title: String(localized: "settings.night_mode.section_title", defaultValue: "Night Mode"),
|
||||||
systemImage: "moon.stars.fill",
|
systemImage: "moon.stars.fill",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -22,8 +22,8 @@ struct NightModeSection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Enable Night Mode",
|
title: String(localized: "settings.night_mode.enable.title", defaultValue: "Enable Night Mode"),
|
||||||
subtitle: "Use a red clock for low light",
|
subtitle: String(localized: "settings.night_mode.enable.subtitle", defaultValue: "Use a red clock for low light"),
|
||||||
isOn: $style.nightModeEnabled,
|
isOn: $style.nightModeEnabled,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -34,8 +34,8 @@ struct NightModeSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Auto Night Mode",
|
title: String(localized: "settings.night_mode.auto.title", defaultValue: "Auto Night Mode"),
|
||||||
subtitle: "Trigger based on ambient light",
|
subtitle: String(localized: "settings.night_mode.auto.subtitle", defaultValue: "Trigger based on ambient light"),
|
||||||
isOn: $style.autoNightMode,
|
isOn: $style.autoNightMode,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -47,8 +47,8 @@ struct NightModeSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsSlider(
|
SettingsSlider(
|
||||||
title: "Light Threshold",
|
title: String(localized: "settings.night_mode.light_threshold.title", defaultValue: "Light Threshold"),
|
||||||
subtitle: "Lower values activate sooner",
|
subtitle: String(localized: "settings.night_mode.light_threshold.subtitle", defaultValue: "Lower values activate sooner"),
|
||||||
value: $style.ambientLightThreshold,
|
value: $style.ambientLightThreshold,
|
||||||
in: 0.1...0.8,
|
in: 0.1...0.8,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
@ -63,8 +63,8 @@ struct NightModeSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Scheduled Night Mode",
|
title: String(localized: "settings.night_mode.scheduled.title", defaultValue: "Scheduled Night Mode"),
|
||||||
subtitle: "Enable on a daily schedule",
|
subtitle: String(localized: "settings.night_mode.scheduled.subtitle", defaultValue: "Enable on a daily schedule"),
|
||||||
isOn: $style.scheduledNightMode,
|
isOn: $style.scheduledNightMode,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -76,7 +76,7 @@ struct NightModeSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("Start Time")
|
Text(String(localized: "settings.night_mode.start_time", defaultValue: "Start Time"))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -91,7 +91,7 @@ struct NightModeSection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("End Time")
|
Text(String(localized: "settings.night_mode.end_time", defaultValue: "End Time"))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -110,7 +110,7 @@ struct NightModeSection: View {
|
|||||||
HStack(spacing: Design.Spacing.xSmall) {
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
Image(systemName: "moon.fill")
|
Image(systemName: "moon.fill")
|
||||||
.foregroundStyle(AppStatus.error)
|
.foregroundStyle(AppStatus.error)
|
||||||
Text("Night Mode Active")
|
Text(String(localized: "settings.night_mode.active", defaultValue: "Night Mode Active"))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundStyle(AppStatus.error)
|
.foregroundStyle(AppStatus.error)
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -121,7 +121,7 @@ struct NightModeSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
.font(.caption)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ struct OverlaySection: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Overlays",
|
title: String(localized: "settings.overlays.section_title", defaultValue: "Overlays"),
|
||||||
systemImage: "rectangle.on.rectangle.angled",
|
systemImage: "rectangle.on.rectangle.angled",
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -24,8 +24,8 @@ struct OverlaySection: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Battery Level",
|
title: String(localized: "settings.overlays.battery_level.title", defaultValue: "Battery Level"),
|
||||||
subtitle: "Show battery percentage",
|
subtitle: String(localized: "settings.overlays.battery_level.subtitle", defaultValue: "Show battery percentage"),
|
||||||
isOn: $style.showBattery,
|
isOn: $style.showBattery,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -36,8 +36,8 @@ struct OverlaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Date",
|
title: String(localized: "settings.overlays.date.title", defaultValue: "Date"),
|
||||||
subtitle: "Display the current date",
|
subtitle: String(localized: "settings.overlays.date.subtitle", defaultValue: "Display the current date"),
|
||||||
isOn: $style.showDate,
|
isOn: $style.showDate,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -48,8 +48,8 @@ struct OverlaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Next Alarm",
|
title: String(localized: "settings.overlays.next_alarm.title", defaultValue: "Next Alarm"),
|
||||||
subtitle: "Show your next scheduled alarm",
|
subtitle: String(localized: "settings.overlays.next_alarm.subtitle", defaultValue: "Show your next scheduled alarm"),
|
||||||
isOn: $style.showNextAlarm,
|
isOn: $style.showNextAlarm,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -60,8 +60,8 @@ struct OverlaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Noise Controls",
|
title: String(localized: "settings.overlays.noise_controls.title", defaultValue: "Noise Controls"),
|
||||||
subtitle: "Mini-player for white noise",
|
subtitle: String(localized: "settings.overlays.noise_controls.subtitle", defaultValue: "Mini-player for white noise"),
|
||||||
isOn: $style.showNoiseControls,
|
isOn: $style.showNoiseControls,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
@ -73,14 +73,14 @@ struct OverlaySection: View {
|
|||||||
.padding(.horizontal, Design.Spacing.medium)
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title: "Date Format",
|
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
|
||||||
subtitle: style.dateFormat,
|
subtitle: style.dateFormat,
|
||||||
backgroundColor: .clear
|
backgroundColor: .clear
|
||||||
) {
|
) {
|
||||||
SettingsSelectionView(
|
SettingsSelectionView(
|
||||||
selection: $style.dateFormat,
|
selection: $style.dateFormat,
|
||||||
options: dateFormats.map { $0.1 },
|
options: dateFormats.map { $0.1 },
|
||||||
title: "Date Format",
|
title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
|
||||||
toString: { format in
|
toString: { format in
|
||||||
dateFormats.first(where: { $0.1 == format })?.0 ?? format
|
dateFormats.first(where: { $0.1 == format })?.0 ?? format
|
||||||
}
|
}
|
||||||
|
|||||||
@ -229,12 +229,22 @@ struct TimeDisplayView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Container: \(Int(containerSize.width))×\(Int(containerSize.height))")
|
Text(
|
||||||
Text("Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))")
|
String(
|
||||||
Text("Font Size: \(Int(fontSize))")
|
localized: "clock.debug.container_size",
|
||||||
Text("Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))")
|
defaultValue: "Container: \(Int(containerSize.width))×\(Int(containerSize.height))"
|
||||||
Text("Portrait: \(portrait ? "true" : "false")")
|
)
|
||||||
Text("Family: \(fontFamily.rawValue)")
|
)
|
||||||
|
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))
|
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||||
.foregroundStyle(.yellow)
|
.foregroundStyle(.yellow)
|
||||||
|
|||||||
@ -134,7 +134,7 @@ struct CategoryTab: View {
|
|||||||
.styled(.subheadingEmphasis)
|
.styled(.subheadingEmphasis)
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
Text("\(count)")
|
Text(count, format: .number)
|
||||||
.styled(.caption, emphasis: .secondary)
|
.styled(.caption, emphasis: .secondary)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
|
|||||||
@ -62,7 +62,11 @@ struct SoundControlView: View {
|
|||||||
.font(.title2.weight(.semibold))
|
.font(.title2.weight(.semibold))
|
||||||
.foregroundStyle(.white)
|
.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))
|
.font(.headline.weight(.semibold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
@ -79,7 +83,11 @@ struct SoundControlView: View {
|
|||||||
.animation(.easeInOut(duration: 0.2), value: isPlaying)
|
.animation(.easeInOut(duration: 0.2), value: isPlaying)
|
||||||
.animation(.easeInOut(duration: 0.2), value: selectedSound)
|
.animation(.easeInOut(duration: 0.2), value: selectedSound)
|
||||||
.accessibilityIdentifier("noise.playStopButton")
|
.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
|
.frame(maxWidth: 400) // Reasonable max width for iPad
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
|
|||||||
@ -44,7 +44,10 @@ struct NoiseView: View {
|
|||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|
||||||
TextField("Search sounds", text: $searchText)
|
TextField(
|
||||||
|
String(localized: "noise.search.placeholder", defaultValue: "Search sounds"),
|
||||||
|
text: $searchText
|
||||||
|
)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.accessibilityIdentifier("noise.searchField")
|
.accessibilityIdentifier("noise.searchField")
|
||||||
@ -79,7 +82,7 @@ struct NoiseView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Noise")
|
.navigationTitle(String(localized: "noise.navigation_title", defaultValue: "Noise"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.animation(.easeInOut(duration: 0.3), value: selectedSound)
|
.animation(.easeInOut(duration: 0.3), value: selectedSound)
|
||||||
.accessibilityIdentifier("noise.screen")
|
.accessibilityIdentifier("noise.screen")
|
||||||
@ -125,11 +128,16 @@ struct NoiseView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("Ready for Sleep?")
|
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
|
||||||
.typography(.title3Bold)
|
.typography(.title3Bold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.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)
|
.typography(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -170,7 +178,7 @@ struct NoiseView: View {
|
|||||||
// Left side: Player controls
|
// Left side: Player controls
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||||
if !isPad {
|
if !isPad {
|
||||||
Text("Ambient Sounds")
|
Text(String(localized: "noise.ambient_sounds", defaultValue: "Ambient Sounds"))
|
||||||
.sectionTitleStyle()
|
.sectionTitleStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,11 +199,11 @@ struct NoiseView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("Ready for Sleep?")
|
Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
|
||||||
.typography(.title3Bold)
|
.typography(.title3Bold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.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)
|
.typography(.caption)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|||||||
@ -37,7 +37,11 @@ struct OnboardingBottomControls: View {
|
|||||||
onSkip()
|
onSkip()
|
||||||
}
|
}
|
||||||
} label: {
|
} 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)
|
.typography(.bodyEmphasis)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -52,7 +56,11 @@ struct OnboardingBottomControls: View {
|
|||||||
onFinish()
|
onFinish()
|
||||||
}
|
}
|
||||||
} label: {
|
} 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)
|
.typography(.bodyEmphasis)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
@ -25,22 +25,22 @@ struct OnboardingGetStartedPage: View {
|
|||||||
.foregroundStyle(AppStatus.success)
|
.foregroundStyle(AppStatus.success)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("You're ready!")
|
Text(String(localized: "onboarding.get_started.title", defaultValue: "You're ready!"))
|
||||||
.typography(.heroBold)
|
.typography(.heroBold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.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)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
OnboardingFeatureRow(icon: "alarm.fill", text: "Create your first alarm")
|
OnboardingFeatureRow(icon: "alarm.fill", text: String(localized: "onboarding.get_started.feature_first_alarm", defaultValue: "Create your first alarm"))
|
||||||
OnboardingFeatureRow(icon: "repeat", text: "Set repeat days (weekdays/weekends)")
|
OnboardingFeatureRow(icon: "repeat", text: String(localized: "onboarding.get_started.feature_repeat_days", defaultValue: "Set repeat days (weekdays/weekends)"))
|
||||||
OnboardingFeatureRow(icon: "slider.horizontal.3", text: "Customize vibration, flash, and volume")
|
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: "Wait 5s for full screen")
|
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: "Tap Noise to play sounds")
|
OnboardingFeatureRow(icon: "speaker.wave.2", text: String(localized: "onboarding.get_started.feature_noise", defaultValue: "Tap Noise to play sounds"))
|
||||||
}
|
}
|
||||||
.padding(.top, Design.Spacing.medium)
|
.padding(.top, Design.Spacing.medium)
|
||||||
|
|
||||||
|
|||||||
@ -33,23 +33,23 @@ struct OnboardingPermissionsPage: View {
|
|||||||
.symbolEffect(.pulse, options: .repeating)
|
.symbolEffect(.pulse, options: .repeating)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Alarms that actually work")
|
Text(String(localized: "onboarding.permissions.title", defaultValue: "Alarms that actually work"))
|
||||||
.typography(.heroBold)
|
.typography(.heroBold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
.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)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
OnboardingFeatureRow(icon: "moon.zzz.fill", text: "Cuts through Do Not Disturb")
|
OnboardingFeatureRow(icon: "moon.zzz.fill", text: String(localized: "onboarding.permissions.feature_dnd", defaultValue: "Cuts through Do Not Disturb"))
|
||||||
OnboardingFeatureRow(icon: "lock.iphone", text: "Shows countdown on Lock Screen")
|
OnboardingFeatureRow(icon: "lock.iphone", text: String(localized: "onboarding.permissions.feature_lock_screen", defaultValue: "Shows countdown on Lock Screen"))
|
||||||
OnboardingFeatureRow(icon: "iphone.badge.play", text: "Works when app is closed")
|
OnboardingFeatureRow(icon: "iphone.badge.play", text: String(localized: "onboarding.permissions.feature_app_closed", defaultValue: "Works when app is closed"))
|
||||||
}
|
}
|
||||||
.padding(.top, Design.Spacing.medium)
|
.padding(.top, Design.Spacing.medium)
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ struct OnboardingPermissionsPage: View {
|
|||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 24))
|
.font(.system(size: 24))
|
||||||
Text("Alarms enabled!")
|
Text(String(localized: "onboarding.permissions.enabled", defaultValue: "Alarms enabled!"))
|
||||||
}
|
}
|
||||||
.foregroundStyle(AppStatus.success)
|
.foregroundStyle(AppStatus.success)
|
||||||
.typography(.bodyEmphasis)
|
.typography(.bodyEmphasis)
|
||||||
@ -89,7 +89,7 @@ struct OnboardingPermissionsPage: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "alarm.fill")
|
Image(systemName: "alarm.fill")
|
||||||
Text("Enable Alarms")
|
Text(String(localized: "onboarding.permissions.enable_alarms", defaultValue: "Enable Alarms"))
|
||||||
}
|
}
|
||||||
.typography(.bodyEmphasis)
|
.typography(.bodyEmphasis)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@ -106,7 +106,7 @@ struct OnboardingPermissionsPage: View {
|
|||||||
|
|
||||||
private var keepAwakeSection: some View {
|
private var keepAwakeSection: some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
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)
|
.typography(.callout)
|
||||||
.foregroundStyle(AppTextColors.tertiary)
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -116,7 +116,11 @@ struct OnboardingPermissionsPage: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Image(systemName: keepAwakeEnabled ? "checkmark.circle.fill" : "bolt.fill")
|
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)
|
.typography(.callout)
|
||||||
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)
|
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)
|
||||||
|
|||||||
@ -20,26 +20,26 @@ struct OnboardingWelcomePage: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, Design.Spacing.medium)
|
.padding(.bottom, Design.Spacing.medium)
|
||||||
|
|
||||||
Text("The Noise Clock")
|
Text(String(localized: "onboarding.welcome.title", defaultValue: "The Noise Clock"))
|
||||||
.typography(.heroBold)
|
.typography(.heroBold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
|
||||||
Text("Your beautiful bedside companion")
|
Text(String(localized: "onboarding.welcome.subtitle", defaultValue: "Your beautiful bedside companion"))
|
||||||
.typography(.title3)
|
.typography(.title3)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
OnboardingFeatureRow(
|
OnboardingFeatureRow(
|
||||||
icon: "moon.stars.fill",
|
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(
|
OnboardingFeatureRow(
|
||||||
icon: "alarm.fill",
|
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(
|
OnboardingFeatureRow(
|
||||||
icon: "clock.fill",
|
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)
|
.padding(.top, Design.Spacing.large)
|
||||||
|
|||||||
4406
TheNoiseClock/Localizable.xcstrings
Normal file
4406
TheNoiseClock/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,10 +16,14 @@ public enum DigitAnimationStyle: String, CaseIterable, Codable {
|
|||||||
|
|
||||||
public var displayName: String {
|
public var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .none: return "None"
|
case .none:
|
||||||
case .spring: return "Spring"
|
return String(localized: "digit_animation.none.display_name", defaultValue: "None")
|
||||||
case .bounce: return "Bounce"
|
case .spring:
|
||||||
case .glitch: return "Glitch"
|
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
|
/// Display name for the category
|
||||||
public var displayName: String {
|
public var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all: return "All"
|
case .all:
|
||||||
case .colored: return "Colored"
|
return String(localized: "sound_category.all.display_name", defaultValue: "All")
|
||||||
case .ambient: return "Ambient"
|
case .colored:
|
||||||
case .nature: return "Nature"
|
return String(localized: "sound_category.colored.display_name", defaultValue: "Colored")
|
||||||
case .mechanical: return "Mechanical"
|
case .ambient:
|
||||||
case .alarm: return "Alarm Sounds"
|
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
|
/// Description of the category
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all: return "All available sounds"
|
case .all:
|
||||||
case .colored: return "Synthetic noise signals for focus, sleep, and relaxation"
|
return String(localized: "sound_category.all.description", defaultValue: "All available sounds")
|
||||||
case .ambient: return "General ambient sounds"
|
case .colored:
|
||||||
case .nature: return "Natural environmental sounds"
|
return String(localized: "sound_category.colored.description", defaultValue: "Synthetic noise signals for focus, sleep, and relaxation")
|
||||||
case .mechanical: return "Mechanical and electronic sounds"
|
case .ambient:
|
||||||
case .alarm: return "Wake-up and notification alarm sounds"
|
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)
|
.symbolEffect(.bounce, options: .nonRepeating)
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.small) {
|
VStack(spacing: Design.Spacing.small) {
|
||||||
Text("Keep Awake for Alarms")
|
Text(String(localized: "keep_awake.prompt.title", defaultValue: "Keep Awake for Alarms"))
|
||||||
.typography(.title2Bold)
|
.typography(.title2Bold)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
.multilineTextAlignment(.center)
|
.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)
|
.typography(.body)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -34,7 +34,7 @@ struct KeepAwakePrompt: View {
|
|||||||
|
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
Button(action: onEnable) {
|
Button(action: onEnable) {
|
||||||
Text("Enable Keep Awake")
|
Text(String(localized: "keep_awake.prompt.enable", defaultValue: "Enable Keep Awake"))
|
||||||
.font(Typography.headingEmphasis.font)
|
.font(Typography.headingEmphasis.font)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -44,7 +44,7 @@ struct KeepAwakePrompt: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(action: onDismiss) {
|
Button(action: onDismiss) {
|
||||||
Text("Not Now")
|
Text(String(localized: "common.not_now", defaultValue: "Not Now"))
|
||||||
.font(Typography.bodyEmphasis.font)
|
.font(Typography.bodyEmphasis.font)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user