Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6c5e0b4d74
commit
3464cbeb17
@ -5,6 +5,7 @@
|
||||
// Created by Matt Bruce on 9/11/25.
|
||||
//
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum FontFamily: String, CaseIterable {
|
||||
case system = "System"
|
||||
@ -33,4 +34,60 @@ enum FontFamily: String, CaseIterable {
|
||||
default: self = .system
|
||||
}
|
||||
}
|
||||
|
||||
var fontWeights: [Font.Weight] {
|
||||
guard let weightDict = Self.fontMapWeights[self.rawValue] else {
|
||||
return []
|
||||
}
|
||||
return Array(weightDict.keys)
|
||||
}
|
||||
|
||||
func fontName(weight: Font.Weight) -> String? {
|
||||
guard let weightDict = Self.fontMapWeights[self.rawValue],
|
||||
let fontName = weightDict[weight] else {
|
||||
return nil
|
||||
}
|
||||
return fontName
|
||||
}
|
||||
|
||||
static internal var fontMapWeights: [String: [Font.Weight: String]] = [
|
||||
FontFamily.helvetica.rawValue: [
|
||||
.regular: "Helvetica",
|
||||
.light: "Helvetica-Light",
|
||||
.bold: "Helvetica-Bold"
|
||||
],
|
||||
FontFamily.arial.rawValue: [
|
||||
.regular: "ArialMT",
|
||||
.bold: "Arial-BoldMT"
|
||||
],
|
||||
FontFamily.timesNewRoman.rawValue: [
|
||||
.regular: "TimesNewRomanPSMT",
|
||||
.bold: "TimesNewRomanPS-BoldMT"
|
||||
],
|
||||
FontFamily.georgia.rawValue: [
|
||||
.regular: "Georgia",
|
||||
.bold: "Georgia-Bold"
|
||||
],
|
||||
FontFamily.verdana.rawValue: [
|
||||
.regular: "Verdana",
|
||||
.bold: "Verdana-Bold"
|
||||
],
|
||||
FontFamily.courier.rawValue: [
|
||||
.regular: "Courier",
|
||||
.bold: "Courier-Bold"
|
||||
],
|
||||
FontFamily.futura.rawValue: [
|
||||
.regular: "Futura-Medium",
|
||||
.bold: "Futura-Bold"
|
||||
],
|
||||
FontFamily.avenir.rawValue: [
|
||||
.regular: "Avenir-Roman",
|
||||
.bold: "Avenir-Bold"
|
||||
],
|
||||
FontFamily.roboto.rawValue: [
|
||||
.regular: "Roboto-Regular",
|
||||
.bold: "Roboto-Bold"
|
||||
]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ struct FontUtils {
|
||||
return .system(size: size, weight: weight, design: design)
|
||||
} else {
|
||||
return .custom(weightedFontName(
|
||||
name: name.rawValue,
|
||||
name: name,
|
||||
weight: weight,
|
||||
design: design
|
||||
), size: size)
|
||||
@ -75,7 +75,7 @@ struct FontUtils {
|
||||
|
||||
if let font = UIFont(
|
||||
name: weightedFontName(
|
||||
name: name.rawValue,
|
||||
name: name,
|
||||
weight: weight,
|
||||
design: design
|
||||
),
|
||||
@ -111,29 +111,193 @@ struct FontUtils {
|
||||
}
|
||||
|
||||
private static func weightedFontName(
|
||||
name: String,
|
||||
name: FontFamily,
|
||||
weight: Font.Weight,
|
||||
design: Font.Design
|
||||
) -> String {
|
||||
let weightSuffix = weight.uiFontWeightSuffix
|
||||
|
||||
// Use exact name from map if available
|
||||
if let exactName = name.fontName(weight: weight) {
|
||||
return exactName
|
||||
}
|
||||
|
||||
// Fallback design handling
|
||||
var baseName = name.rawValue
|
||||
switch design {
|
||||
case .rounded:
|
||||
if name.lowercased() == "system" { return "System" }
|
||||
return name
|
||||
+ (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded")
|
||||
baseName = "ArialRoundedMTBold" // System rounded fallback
|
||||
case .monospaced:
|
||||
if name.lowercased() == "system" { return "Courier" }
|
||||
return name == "Courier"
|
||||
? name + weightSuffix
|
||||
: name
|
||||
+ (weightSuffix.isEmpty ? "-Mono" : weightSuffix + "Mono")
|
||||
baseName = "Courier"
|
||||
case .serif:
|
||||
if name.lowercased() == "system" { return "TimesNewRomanPS" }
|
||||
return name + weightSuffix
|
||||
baseName = "TimesNewRomanPSMT"
|
||||
default:
|
||||
return name + weightSuffix
|
||||
break
|
||||
}
|
||||
|
||||
let weightSuffix = weight.uiFontWeightSuffix
|
||||
return baseName + (weightSuffix.isEmpty ? "" : "-" + weightSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TestContentView: View {
|
||||
@State private var digit: String = "138"
|
||||
@State private var previewFontSize: CGFloat = 1000
|
||||
@State private var fontWeight: Font.Weight = .bold
|
||||
@State private var fontDesign: Font.Design = .rounded
|
||||
|
||||
private var date: Date {
|
||||
let hours = ["13"].randomElement() ?? "08"
|
||||
let minutes = ["38"].randomElement() ?? "33"
|
||||
let timeString = "\(hours):\(minutes)"
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let todayString = formatter.string(from: Date())
|
||||
let fullDateString = "\(todayString) \(timeString)"
|
||||
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
||||
return formatter.date(from: fullDateString) ?? Date()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let newDate = date
|
||||
return ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
ForEach(FontFamily.allCases, id: \.rawValue) { (font: FontFamily) in
|
||||
VStack {
|
||||
Text(font.rawValue)
|
||||
.font(Font.system(size: 16, weight: Font.Weight.bold))
|
||||
.padding(.bottom, 5)
|
||||
|
||||
TimeDisplayView(date: newDate,
|
||||
use24Hour: true,
|
||||
showSeconds: false,
|
||||
digitColor: .primary,
|
||||
glowIntensity: 0.5,
|
||||
manualScale: 1.0,
|
||||
stretched: false,
|
||||
clockOpacity: 1.0,
|
||||
fontFamily: font,
|
||||
fontWeight: fontWeight,
|
||||
fontDesign: fontDesign,
|
||||
forceHorizontalMode: true)
|
||||
|
||||
TimeSegment(
|
||||
text: digit,
|
||||
fontSize: .constant(129), // CGFloat
|
||||
opacity: 1.0, // Double
|
||||
digitColor: Color.primary, // Color
|
||||
glowIntensity: 0.5, // Double
|
||||
fontFamily: font, // FontFamily
|
||||
fontWeight: fontWeight,
|
||||
fontDesign: fontDesign,
|
||||
)
|
||||
}
|
||||
.frame(width: 400, height: 200)
|
||||
.border(Color.black)
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FontCombinationsPreview: View {
|
||||
// All designs
|
||||
private let designs: [Font.Design] = Font.Design.allCases
|
||||
|
||||
private let columns: [GridItem] = [
|
||||
GridItem(.flexible(), spacing: 20),
|
||||
GridItem(.flexible(), spacing: 20),
|
||||
GridItem(.flexible(), spacing: 20)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
// Precompute local copies to avoid type-checker stress
|
||||
let fonts: [FontFamily] = FontFamily.allCases.sorted {
|
||||
$0.rawValue.localizedCaseInsensitiveCompare($1.rawValue)
|
||||
== .orderedAscending
|
||||
}
|
||||
let designsLocal: [Font.Design] = designs
|
||||
|
||||
return ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 20) {
|
||||
ForEach(fonts, id: \.self) { font in
|
||||
ForEach(font.fontWeights, id: \.self) { weight in
|
||||
ForEach(designsLocal, id: \.self) { design in
|
||||
FontSampleCell(font: font, weight: weight, design: design)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FontSampleCell: View {
|
||||
let font: FontFamily
|
||||
let weight: Font.Weight
|
||||
let design: Font.Design
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 5) {
|
||||
Text("\(font.rawValue), \(weight.rawValue), \(design.rawValue)")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 5)
|
||||
TimeSegment(
|
||||
text: "38",
|
||||
fontSize: .constant(129), // CGFloat
|
||||
opacity: 1.0, // Double
|
||||
digitColor: Color.primary, // Color
|
||||
glowIntensity: 0.5, // Double
|
||||
fontFamily: font, // FontFamily
|
||||
fontWeight: weight,
|
||||
fontDesign: design)
|
||||
.frame(width: 100, height: 100)
|
||||
.border(Color.black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
FontCombinationsPreview()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Descriptions for Font.Weight and Font.Design
|
||||
private extension Font.Weight {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .ultraLight: return "ultraLight"
|
||||
case .thin: return "thin"
|
||||
case .light: return "light"
|
||||
case .regular: return "regular"
|
||||
case .medium: return "medium"
|
||||
case .semibold: return "semibold"
|
||||
case .bold: return "bold"
|
||||
case .heavy: return "heavy"
|
||||
case .black: return "black"
|
||||
default:
|
||||
// Fallback for any future/unknown cases
|
||||
return "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Font.Design {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .default: return "default"
|
||||
case .serif: return "serif"
|
||||
case .rounded: return "rounded"
|
||||
case .monospaced: return "monospaced"
|
||||
@unknown default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -69,7 +69,42 @@ class ClockViewModel {
|
||||
}
|
||||
|
||||
func updateStyle(_ newStyle: ClockStyle) {
|
||||
style = newStyle
|
||||
DebugLogger.log("ClockViewModel: updateStyle called", category: .settings)
|
||||
DebugLogger.log("ClockViewModel: old fontFamily = \(style.fontFamily)", category: .settings)
|
||||
DebugLogger.log("ClockViewModel: new fontFamily = \(newStyle.fontFamily)", category: .settings)
|
||||
|
||||
// Update properties of the existing style object instead of replacing it
|
||||
// This preserves the @Observable chain
|
||||
style.use24Hour = newStyle.use24Hour
|
||||
style.showSeconds = newStyle.showSeconds
|
||||
style.forceHorizontalMode = newStyle.forceHorizontalMode
|
||||
style.digitColorHex = newStyle.digitColorHex
|
||||
style.glowIntensity = newStyle.glowIntensity
|
||||
style.digitScale = newStyle.digitScale
|
||||
style.stretched = newStyle.stretched
|
||||
style.clockOpacity = newStyle.clockOpacity
|
||||
style.fontFamily = newStyle.fontFamily
|
||||
style.fontWeight = newStyle.fontWeight
|
||||
style.fontDesign = newStyle.fontDesign
|
||||
style.showBattery = newStyle.showBattery
|
||||
style.showDate = newStyle.showDate
|
||||
style.overlayOpacity = newStyle.overlayOpacity
|
||||
style.backgroundHex = newStyle.backgroundHex
|
||||
style.keepAwake = newStyle.keepAwake
|
||||
style.randomizeColor = newStyle.randomizeColor
|
||||
style.selectedColorTheme = newStyle.selectedColorTheme
|
||||
style.nightModeEnabled = newStyle.nightModeEnabled
|
||||
style.autoNightMode = newStyle.autoNightMode
|
||||
style.scheduledNightMode = newStyle.scheduledNightMode
|
||||
style.nightModeStartTime = newStyle.nightModeStartTime
|
||||
style.nightModeEndTime = newStyle.nightModeEndTime
|
||||
style.ambientLightThreshold = newStyle.ambientLightThreshold
|
||||
style.autoBrightness = newStyle.autoBrightness
|
||||
style.dateFormat = newStyle.dateFormat
|
||||
style.respectFocusModes = newStyle.respectFocusModes
|
||||
|
||||
DebugLogger.log("ClockViewModel: after update fontFamily = \(style.fontFamily)", category: .settings)
|
||||
|
||||
saveStyle()
|
||||
updateTimersIfNeeded()
|
||||
updateWakeLockState()
|
||||
|
||||
@ -235,6 +235,15 @@ private struct FontSection: View {
|
||||
|
||||
// Use the enum allCases for font options
|
||||
|
||||
// Computed property for available weights based on selected font
|
||||
private var availableWeights: [Font.Weight] {
|
||||
if style.fontFamily == .system {
|
||||
return Font.Weight.allCases
|
||||
} else {
|
||||
return style.fontFamily.fontWeights
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Font")) {
|
||||
// Font Family
|
||||
@ -244,22 +253,36 @@ private struct FontSection: View {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.onChange(of: style.fontFamily) { _, newFamily in
|
||||
// Auto-set design to default for non-system fonts
|
||||
if newFamily != .system {
|
||||
style.fontDesign = .default
|
||||
}
|
||||
|
||||
// Font Weight
|
||||
// Auto-set weight to first available weight if current weight is not available
|
||||
let availableWeights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights
|
||||
if !availableWeights.contains(style.fontWeight) {
|
||||
style.fontWeight = availableWeights.first ?? .regular
|
||||
}
|
||||
}
|
||||
|
||||
// Font Weight - show available weights for selected font
|
||||
Picker("Weight", selection: $style.fontWeight) {
|
||||
ForEach(Font.Weight.allCases, id: \.self) { weight in
|
||||
ForEach(availableWeights, id: \.self) { weight in
|
||||
Text(weight.rawValue).tag(weight)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
// Font Design
|
||||
Picker("Design", selection: $style.fontDesign) {
|
||||
ForEach(Font.Design.allCases, id: \.self) { design in
|
||||
Text(design.rawValue).tag(design)
|
||||
// Font Design - only show for system font
|
||||
if style.fontFamily == .system {
|
||||
Picker("Design", selection: $style.fontDesign) {
|
||||
ForEach(Font.Design.allCases, id: \.self) { design in
|
||||
Text(design.rawValue).tag(design)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
// Font Preview
|
||||
HStack {
|
||||
|
||||
@ -17,7 +17,9 @@ struct ClockDisplayContainer: View {
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
DebugLogger.log("ClockDisplayContainer: body called with fontFamily=\(style.fontFamily), fontWeight=\(style.fontWeight), fontDesign=\(style.fontDesign)", category: .general)
|
||||
|
||||
return GeometryReader { geometry in
|
||||
let isPortrait = geometry.size.height >= geometry.size.width
|
||||
let hasOverlay = style.showBattery || style.showDate
|
||||
let topSpacing = hasOverlay ? (isPortrait ? UIConstants.Spacing.huge : UIConstants.Spacing.large) : 0
|
||||
|
||||
@ -11,13 +11,13 @@ import SwiftUI
|
||||
struct DigitView: View {
|
||||
@Environment(\.sizeCategory) private var sizeCategory
|
||||
|
||||
@State var digit: String
|
||||
@State var fontName: FontFamily
|
||||
@State var weight: Font.Weight
|
||||
@State var design: Font.Design
|
||||
@State var opacity: Double
|
||||
@State var digitColor: Color
|
||||
@State var glowIntensity: Double
|
||||
let digit: String
|
||||
let fontName: FontFamily
|
||||
let weight: Font.Weight
|
||||
let design: Font.Design
|
||||
let opacity: Double
|
||||
let digitColor: Color
|
||||
let glowIntensity: Double
|
||||
@Binding var fontSize: CGFloat
|
||||
|
||||
@State private var lastCalculatedSize: CGSize = .zero
|
||||
@ -31,6 +31,7 @@ struct DigitView: View {
|
||||
opacity: Double = 1,
|
||||
glowIntensity: Double = 0,
|
||||
fontSize: Binding<CGFloat>) {
|
||||
DebugLogger.log("DigitView: init called with fontName=\(fontName), weight=\(weight), design=\(design)", category: .general)
|
||||
self.digit = (digit.count == 1 && "0123456789".contains(digit)) ? digit : "0"
|
||||
self.fontName = fontName
|
||||
self.weight = weight
|
||||
@ -96,6 +97,18 @@ struct DigitView: View {
|
||||
.onChange(of: sizeCategory) { _, _ in
|
||||
calculateOptimalFontSize(for: geometry.size)
|
||||
}
|
||||
.onChange(of: fontName) { _, _ in
|
||||
DebugLogger.log("DigitView: fontName changed to \(fontName)", category: .general)
|
||||
calculateOptimalFontSize(for: geometry.size)
|
||||
}
|
||||
.onChange(of: weight) { _, _ in
|
||||
DebugLogger.log("DigitView: weight changed to \(weight)", category: .general)
|
||||
calculateOptimalFontSize(for: geometry.size)
|
||||
}
|
||||
.onChange(of: design) { _, _ in
|
||||
DebugLogger.log("DigitView: design changed to \(design)", category: .general)
|
||||
calculateOptimalFontSize(for: geometry.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,9 @@ struct TimeDisplayView: View {
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
DebugLogger.log("TimeDisplayView: body called with fontFamily=\(fontFamily), fontWeight=\(fontWeight), fontDesign=\(fontDesign)", category: .general)
|
||||
|
||||
return GeometryReader { proxy in
|
||||
let containerSize = proxy.size
|
||||
let portraitMode = containerSize.height >= containerSize.width
|
||||
let portrait = !forceHorizontalMode && containerSize.height >= containerSize.width
|
||||
|
||||
@ -20,7 +20,8 @@ struct TimeSegment: View {
|
||||
let fontDesign: Font.Design
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
DebugLogger.log("TimeSegment: body called with fontFamily=\(fontFamily), fontWeight=\(fontWeight), fontDesign=\(fontDesign)", category: .general)
|
||||
return HStack(alignment: .center, spacing: 0) {
|
||||
ForEach(Array(text.enumerated()), id: \.offset) { index, character in
|
||||
DigitView(
|
||||
digit: String(character),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user