mycode/myApp/HaruDanim/Shared/Settings.swift
songyc macbook 49dd1d0ff2 feat(widgets): complete redesign of 5 widget types with modular Intents, fixed floating menu UI/Strings
Widget system rebuilt as 5 independent widgets, each with its own
WidgetConfigurationIntent registered in the WidgetBundle:

- ① ActionRun: multi-action selection, value display option (day/week/
  month/hidden), full tag-color cells; small 1 / medium 4 (2x2) / large
  4 big cells or 8 compact cells (2x4) via largeStyle. Unfilled slots
  render as dashed blank cells.
- ② GoalProgress: multi-goal selection; medium [2 goals | goal+top
  quests], large [4 goals | 2x goal+quests | 1 goal + all quests] via
  medium/largeStyle. New QuestBarRow / GoalWithQuestsCellView.
- ③ QuestRing: multi-quest selection + span; surface-card ring (tag
  color) with centered icon, name and "오늘 N%" text; interactive run
  via RunActionIntent; small 1 / medium 2 / large 4 (2x2).
- ④ QuestStatus (display-only): goal selection + span; rings with name
  only (no %), goal icon+name top-left; small 4 (2x2) / medium 8 (4x2) /
  large [16 (4x4) | 4 goals | 2 goals] via largeStyle.
- ⑤ StatsChart: multi-action selection capped per family (2/4/6) +
  3 span modes; same design scaled by family.

Common foundation:
- Theme option revived on every intent: match app / light fixed / dark
  fixed (WidgetThemeOption AppEnum) applied through widgetAppTheme(_:)
  with proper iOS 17 containerBackground; non-fullColor rendering modes
  (tinted/clear/accented) keep the Color.clear + semantic-color defense
  so content never goes white-on-white.
- Providers are family-aware (capacity by size) and slot-pad selections
  (WidgetStore.slots); defaults fill from existing data so fresh widgets
  never look empty.
- Interactive path unchanged: RunActionIntent(actionID:) +
  IntentStore.performWrite -> reloadAllTimelines.

Floating menu polish:
- All glass capsules now share a uniform width (longest label wins,
  locale-proof) for a tidy popped-up list.
- Settings strings updated: nav style renamed to '플로팅 글래스 캡슐'
  with description matching the capsule popup, order editor retitled
  '캡슐 메뉴 순서' with bottom-to-top wording.

Verified: BUILD SUCCEEDED (iPhone 17 Pro sim); widget preview harness
extended with per-variant scroll anchors and all 15 layout variants
screenshot-checked, including forced accented mode (legible translucent
cells) and blank-slot rendering; capsule menu and settings text
screenshot-checked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-13 01:17:39 +09:00

143 lines
5.1 KiB
Swift

//
// Settings.swift
// Haru_Danim
//
// , (CLAUDE.md §6.6, §7)
//
import Foundation
enum SettingsKeys {
/// "light" | "dark"
static let theme = "settings.theme"
/// "ko" | "en" | "ja"
static let language = "settings.language"
/// Calendar.weekday (1= ... 7=). 2()
static let weekStartWeekday = "settings.weekStartWeekday"
/// . 0 (00:00)
static let dayStartMinutes = "settings.dayStartMinutes"
/// "latest"( ) | "earliest"( ). latest
static let liveActivityMode = "settings.liveActivityMode"
// Shared/Premium.swift PremiumGate.cacheKey (App Group defaults)
/// . 3
static let gridColumns = "settings.gridColumns"
/// () . 0 =
static let minSessionSeconds = "settings.minSessionSeconds"
/// ( 1~3). AppTab rawValue
static let visibleTabs = "settings.visibleTabs"
/// . GoalCardStyle rawValue ("perQuest" | "combined")
static let goalCardStyle = "settings.goalCardStyle"
/// iPhone . NavStyle rawValue ("tabBar" | "radial"). ()
static let navStyle = "settings.navStyle"
/// . AppTab rawValue . ()
static let radialTabOrder = "settings.radialTabOrder"
}
/// iPhone (iPad·Mac , )
enum NavStyle: String, CaseIterable, Identifiable {
/// ( + )
case tabBar
/// (Floating Glass Capsules) rawValue radial
case radial
var id: String { rawValue }
var label: String {
switch self {
case .tabBar: return String(localized: "기본 하단 탭 바")
case .radial: return String(localized: "플로팅 글래스 캡슐")
}
}
}
/// ( )
enum GoalCardStyle: String, CaseIterable, Identifiable {
/// ( 3) //
case perQuest
/// // ()
case combined
var id: String { rawValue }
var label: String {
switch self {
case .perQuest: return String(localized: "다짐별로 각각")
case .combined: return String(localized: "전체 다짐 합산")
}
}
}
enum TabBarConfig {
/// : · · (+)
static let defaultVisibleTabs = "main,action,history"
static let maxVisible = 3
static let minVisible = 1
}
/// ()
enum MinSessionOption {
static let choices: [(seconds: Int, label: String)] = [
(0, String(localized: "사용 안 함")),
(3, String(localized: "3초")),
(5, String(localized: "5초")),
(10, String(localized: "10초")),
(30, String(localized: "30초")),
(60, String(localized: "1분")),
]
}
enum LiveActivityMode: String, CaseIterable, Identifiable {
case latest
case earliest
var id: String { rawValue }
var label: String {
switch self {
case .latest: return String(localized: "가장 나중에 시작한 행동")
case .earliest: return String(localized: "가장 먼저 시작한 행동")
}
}
}
enum AppLanguage: String, CaseIterable, Identifiable {
case ko, en, ja
var id: String { rawValue }
var label: String {
switch self {
case .ko: return "한국어"
case .en: return "English"
case .ja: return "日本語"
}
}
}
// (FreeLimits) Shared/Premium.swift (· )
///
struct TrackingSettings {
var weekStartWeekday: Int
var dayStartMinutes: Int
static func current() -> TrackingSettings {
// App Group (· ), standard
let group = AppGroup.defaults
let standard = UserDefaults.standard
func intValue(_ key: String, default defaultValue: Int) -> Int {
(group.object(forKey: key) ?? standard.object(forKey: key)) as? Int ?? defaultValue
}
return TrackingSettings(
weekStartWeekday: intValue(SettingsKeys.weekStartWeekday, default: 2),
dayStartMinutes: intValue(SettingsKeys.dayStartMinutes, default: 0)
)
}
var calendar: Calendar {
var cal = Calendar.current
cal.firstWeekday = weekStartWeekday
return cal
}
}