Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions MinimalCountdown.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
Models/EffectStyle.swift,
Models/FontWeight.swift,
Models/LegacySettings.swift,
Models/NumeralSystem.swift,
Models/SafeIntDecodable.swift,
Models/SaverSettings.swift,
Models/Schedule.swift,
Expand Down Expand Up @@ -202,6 +203,11 @@
de,
es,
fr,
ar,
he,
fa,
hi,
sa,
);
mainGroup = E9B531912B854D60002A2B79;
productRefGroup = E9B5319C2B854D60002A2B79 /* Products */;
Expand Down
21 changes: 21 additions & 0 deletions MinimalCountdown/ConfigureView/ConfigurationWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ private extension ConfigurationWindow {
}
}
.help(Text(Resources.Digits.roundedHint))

Picker(selection: $settings.typography.numeralSystem) {
ForEach(NumeralSystem.allCases) { system in
Text(numeralRow(system)).tag(system)
}
} label: {
Text(Resources.Digits.numerals)
Text(Resources.Digits.numeralsHint)
.subtitleFont
}
.help(Text(Resources.Digits.numeralsHint))
} header: {
Text(Resources.Digits.title)
}
Expand Down Expand Up @@ -224,6 +235,16 @@ private extension ConfigurationWindow {
return Text(row)
}

/// Picker row: the numeral-system name + a `123` glyph sample for the explicit systems.
/// `.automatic` shows the name only — its glyphs depend on the viewer's locale.
func numeralRow(_ system: NumeralSystem) -> String {
let name = String(localized: system.label)
guard let numbering = system.numberingSystem else { return name }
var components = Locale.Components(identifier: "en_US")
components.numberingSystem = numbering
return "\(name) \(UIModel.formattedDigits(123, in: Locale(components: components)))"
}

func prepareForDisplay() {
withAnimation(.none) {
settings = settingsManager.settings
Expand Down
2 changes: 2 additions & 0 deletions MinimalCountdown/Extensions/CGFloat+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ extension CGFloat {

/// Label text size relative to the digits baseline. CGFloat = 1.0 / 5.0
static let labelsToDigitsRatio: CGFloat = 1.0 / 5.0
/// Non Latin label text size relative to the digits baseline. CGFloat = 1.0 / 3.8
static let nonLatinLabelsToDigitsRatio: CGFloat = 1.0 / 3.8
/// Title text size relative to the digits baseline. CGFloat = 1.0 / 4.0
static let titleToDigitsRatio: CGFloat = 1.0 / 4.0
/// Inter-element spacing as a fraction of window width. CGFloat = 0.04
Expand Down
15 changes: 7 additions & 8 deletions MinimalCountdown/Extensions/Date+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import Foundation

extension Date {
// MARK: - Duration components relative to a reference `now` for target date
func daysString(relativeTo now: Date) -> String { self.duration(from: now, for: .days) }
func hoursString(relativeTo now: Date) -> String { self.duration(from: now, for: .hours) }
func minutesString(relativeTo now: Date) -> String { self.duration(from: now, for: .minutes) }
func secondsString(relativeTo now: Date) -> String { self.duration(from: now, for: .seconds) }
func days(relativeTo now: Date) -> Int { self.duration(from: now, for: .days) }
func hours(relativeTo now: Date) -> Int { self.duration(from: now, for: .hours) }
func minutes(relativeTo now: Date) -> Int { self.duration(from: now, for: .minutes) }
func seconds(relativeTo now: Date) -> Int { self.duration(from: now, for: .seconds) }

// MARK: - DatePicker and date verification components
func datesInBetween() -> ClosedRange<Date> { minDate ... maxDate }
Expand All @@ -23,19 +23,18 @@ extension Date {

// MARK: - Private helpers
private extension Date {
func duration(from now: Date, for element: AppearanceStyle) -> String {
func duration(from now: Date, for element: AppearanceStyle) -> Int {
// use ceil to fix double-zero issue
let total = Int(abs(ceil(self.timeIntervalSince(now))))

let elementDuration: Int = switch element {
return switch element {
case .days: total / .oneDay
case .hours: total % .oneDay / .oneHour
case .minutes: total % .oneHour / .oneMinute
case .seconds: total % .oneMinute
}
return String(format: "%02d", elementDuration)
}

func farAvailableDate(isFuture: Bool) -> Self {
Date(timeInterval: (isFuture ? .oneYearAndOneDay : -.oneYearAndOneDay), since: self)
}
Expand Down
43 changes: 43 additions & 0 deletions MinimalCountdown/Models/NumeralSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// NumeralSystem.swift
// MinimalCountdown
//
// Created by Sergey Kemenov
//

import Foundation

enum NumeralSystem: Int, CaseIterable, Identifiable, SafeIntDecodable, Equatable, Hashable {
case automatic = 0, latin, arabic, persian, devanagari

// safe and un-optional, defaults to .automatic
init(_ rawValue: Int) {
self = .init(rawValue: rawValue) ?? .automatic
}
}

extension NumeralSystem {
var id: Self { self }

/// `nil` for `.automatic` — the caller falls back to the resolved locale's own numbering
/// system (UAE → Latin, Egypt → Arabic). The explicit cases override that.
var numberingSystem: Locale.NumberingSystem? {
switch self {
case .automatic: nil
case .latin: Locale.NumberingSystem("latn")
case .arabic: Locale.NumberingSystem("arab")
case .persian: Locale.NumberingSystem("arabext")
case .devanagari: Locale.NumberingSystem("deva")
}
}

var label: LocalizedStringResource {
switch self {
case .automatic: Resources.Labels.NumeralSystem.automatic
case .latin: Resources.Labels.NumeralSystem.latin
case .arabic: Resources.Labels.NumeralSystem.arabic
case .persian: Resources.Labels.NumeralSystem.persian
case .devanagari: Resources.Labels.NumeralSystem.devanagari
}
}
}
6 changes: 3 additions & 3 deletions MinimalCountdown/Models/SaverSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ extension SaverSettings {
effect: .none,
effectColor: .white
),
typography: .init(weight: .ultraLight, isRounded: false),
typography: .init(weight: .ultraLight, isRounded: false, numeralSystem: .automatic),
title: .init(text: "", isHidden: true)
)
}

#if DEBUG
extension SaverSettings {
static let preview = SaverSettings(
appearance: .init(style: .days, isLabelHidden: false),
appearance: .init(style: .seconds, isLabelHidden: false),
schedule: .init(target: Date(timeIntervalSinceNow: .oneDay * 365)),
theme: .init(
accent: .white,
Expand All @@ -54,7 +54,7 @@ extension SaverSettings {
effect: .none,
effectColor: .white
),
typography: .init(weight: .ultraLight, isRounded: false),
typography: .init(weight: .ultraLight, isRounded: false, numeralSystem: .automatic),
title: .init(text: "See you soon", isHidden: false)
)
}
Expand Down
12 changes: 12 additions & 0 deletions MinimalCountdown/Models/Typography.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,16 @@ import Foundation
struct Typography: Codable, Equatable, Hashable {
var weight: FontWeight
var isRounded: Bool
var numeralSystem: NumeralSystem = .automatic
}

extension Typography {
// Settings written before the Numerals picker have no `numeralSystem` key — decode it
// tolerantly so existing v2 files still load (no schema version bump needed).
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
weight = try container.decode(FontWeight.self, forKey: .weight)
isRounded = try container.decode(Bool.self, forKey: .isRounded)
numeralSystem = try container.decodeIfPresent(NumeralSystem.self, forKey: .numeralSystem) ?? .automatic
}
}
Loading