diff --git a/Chinendar.xcodeproj/project.pbxproj b/Chinendar.xcodeproj/project.pbxproj index 49a2c3e..2fe97c6 100644 --- a/Chinendar.xcodeproj/project.pbxproj +++ b/Chinendar.xcodeproj/project.pbxproj @@ -151,6 +151,9 @@ B39B90682B0181DA0083D05A /* LayoutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833392A46685200E36989 /* LayoutSetting.swift */; }; B39B90692B0181DE0083D05A /* Datetime.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833432A46695000E36989 /* Datetime.swift */; }; B39B906A2B0181E00083D05A /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833452A4739FD00E36989 /* Location.swift */; }; + B39B9DC62B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; + B39B9DC72B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; + B39B9DC82B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; B3BCCEE82A48746000F5745E /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BCCEE72A48746000F5745E /* Setting.swift */; }; B3BEB4C32A48994C000751D5 /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BEB4C22A48994C000751D5 /* WatchFace.swift */; }; B3BEB4C42A489A0A000751D5 /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; @@ -359,6 +362,7 @@ B39B905D2B0179B70083D05A /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; B39B905F2B017AB10083D05A /* Chinendar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Chinendar.entitlements; sourceTree = ""; }; B39B90612B01809A0083D05A /* layout.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = layout.txt; sourceTree = ""; }; + B39B9DC52B57578300D29D60 /* Decoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decoration.swift; sourceTree = ""; }; B3BCCEE72A48746000F5745E /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; B3BEB4C22A48994C000751D5 /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; sourceTree = ""; }; B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivity.swift; sourceTree = ""; }; @@ -456,6 +460,7 @@ B328A2EE2A3D19A4002191F4 /* ThemesList.swift */, B328333B2A46687D00E36989 /* ColorSetting.swift */, B328333D2A4668B000E36989 /* RingSetting.swift */, + B39B9DC52B57578300D29D60 /* Decoration.swift */, B32833412A46691800E36989 /* Documentation.swift */, B32833392A46685200E36989 /* LayoutSetting.swift */, B32833432A46695000E36989 /* Datetime.swift */, @@ -994,6 +999,7 @@ B39B905E2B0179B70083D05A /* Layout.swift in Sources */, B39B90652B0181D20083D05A /* ColorSetting.swift in Sources */, B39B90502B0178DF0083D05A /* ThemeData.swift in Sources */, + B39B9DC82B57578300D29D60 /* Decoration.swift in Sources */, B39B90582B01794D0083D05A /* SwiftUIUtilities.swift in Sources */, B39B90692B0181DE0083D05A /* Datetime.swift in Sources */, B39B90682B0181DA0083D05A /* LayoutSetting.swift in Sources */, @@ -1038,6 +1044,7 @@ B32833402A4668EA00E36989 /* Welcome.swift in Sources */, 9E5A41222AA61FC400B470BE /* Relevance.swift in Sources */, B37063A429FAFF3300CC6E57 /* MetaLayout.swift in Sources */, + B39B9DC72B57578300D29D60 /* Decoration.swift in Sources */, B3515CF329F6149500E6BCDC /* Layout.swift in Sources */, B3BEB4C52A489A10000751D5 /* WatchFaceView.swift in Sources */, B328333A2A46685200E36989 /* LayoutSetting.swift in Sources */, @@ -1164,6 +1171,7 @@ D2F0825D26FAB23500ADBE13 /* Data.swift in Sources */, D2E4E0E626F7C73E002F3716 /* macApp.swift in Sources */, B37063A329FAFF3300CC6E57 /* MetaLayout.swift in Sources */, + B39B9DC62B57578300D29D60 /* Decoration.swift in Sources */, 9EBFBE332A58A40900DC42AF /* ThemeData.swift in Sources */, B32999242A4F989600B71579 /* SwiftUIUtilities.swift in Sources */, B32999222A4F96D600B71579 /* Setting.swift in Sources */, @@ -1668,8 +1676,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 106; - APP_VERSION = 5.2; + APP_BUILD = 113; + APP_VERSION = 5.3; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1747,8 +1755,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 106; - APP_VERSION = 5.2; + APP_BUILD = 113; + APP_VERSION = 5.3; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; diff --git a/Shared/DataModel/MetaLayout.swift b/Shared/DataModel/MetaLayout.swift index 4596f5f..cf819fc 100644 --- a/Shared/DataModel/MetaLayout.swift +++ b/Shared/DataModel/MetaLayout.swift @@ -225,6 +225,7 @@ extension String { @ObservationIgnored var initialized = false var globalMonth: Bool = false var apparentTime: Bool = false + var largeHour: Bool = false var locationEnabled: Bool = true var location: CGPoint? = nil var firstRing = Gradient(locations: [0, 1], colors: [CGColor(gray: 1, alpha: 1), CGColor(gray: 1, alpha: 1)], loop: false) @@ -268,6 +269,7 @@ extension String { if includeConfig { encoded += "globalMonth: \(globalMonth)\n" encoded += "apparentTime: \(apparentTime)\n" + encoded += "largeHour: \(largeHour)\n" encoded += "locationEnabled: \(locationEnabled)\n" if let location = location { encoded += "customLocation: \(location.encode())\n" @@ -351,6 +353,7 @@ extension String { globalMonth = values["globalMonth"]?.boolValue ?? globalMonth apparentTime = values["apparentTime"]?.boolValue ?? apparentTime + largeHour = values["largeHour"]?.boolValue ?? largeHour locationEnabled = values["locationEnabled"]?.boolValue ?? locationEnabled location = CGPoint(from: values["customLocation"]) firstRing = readGradient(value: values["firstRing"]) ?? firstRing diff --git a/Shared/DataModel/Model.swift b/Shared/DataModel/Model.swift index 9153cec..f0d34b9 100644 --- a/Shared/DataModel/Model.swift +++ b/Shared/DataModel/Model.swift @@ -10,6 +10,7 @@ import Observation import os.lock infix operator %% +infix operator /% protocol NamedPoint { var name: String { get } @@ -30,6 +31,16 @@ extension FloatingPoint { } } +extension BinaryInteger { + static func /%(_ left: Self, _ right: Self) -> Self { + if left < 0 { + return (left - right + 1) / right + } else { + return left / right + } + } +} + func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } @@ -464,7 +475,7 @@ extension Array { static let day_chinese_compact = ["㏠", "㏡", "㏢", "㏣", "㏤", "㏥", "㏦", "㏧", "㏨", "㏩", "㏪", "㏫", "㏬", "㏭", "㏮", "㏯", "㏰", "㏱", "㏲", "㏳", "㏴", "㏵", "㏶", "㏷", "㏸", "㏹", "㏺", "㏻", "㏼", "㏽"] static let terrestrial_branches = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"] static let sub_hour_name = ["初", "正"] - static let chinese_numbers = ["初", "一", "二", "三", "四", "五"] + static let chinese_numbers = ["初", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", "十三", "十四", "十五", "十六"] static let evenSolarTermChinese = ["冬 至", "大 寒", "雨 水", "春 分", "穀 雨", "小 滿", "夏 至", "大 暑", "處 暑", "秋 分", "霜 降", "小 雪"] static let oddSolarTermChinese = ["小 寒", "立 春", "驚 蟄", "清 明", "立 夏", "芒 種", "小 暑", "立 秋", "白 露", "寒 露", "立 冬", "大 雪"] static let leapLabel = "閏" @@ -496,6 +507,7 @@ extension Array { @ObservationIgnored private let _compact: Bool private var _globalMonth: Bool = false private var _apparentTime: Bool = false + private var _largeHour: Bool = false private var _time: Date = .distantPast private var _calendar: Calendar = .utcCalendar private var _location: CGPoint? = nil @@ -515,11 +527,17 @@ extension Array { @ObservationIgnored private var _moonTimes: [Date?] = [] @ObservationIgnored private var _startHour: Date = .distantPast @ObservationIgnored private var _endHour: Date = .distantFuture - @ObservationIgnored private var _hourNames: [String] = [] + @ObservationIgnored private var _hourNames: [NamedHour] = [] @ObservationIgnored private var _hour_string: String = "" @ObservationIgnored private var _quarter_string: String = "" @ObservationIgnored private let _lock = OSAllocatedUnfairLock() + struct NamedHour { + let hour: Date + let shortName: String + let longName: String + } + struct NamedPosition: NamedPoint { let name: String let pos: Double @@ -563,18 +581,20 @@ extension Array { self._location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location self._globalMonth = WatchLayout.shared.globalMonth self._apparentTime = WatchLayout.shared.apparentTime + self._largeHour = WatchLayout.shared.largeHour updateYear() updateDate() updateHour() } - private init(compact: Bool, time: Date, calendar: Calendar, location: CGPoint?, globalMonth: Bool, apparentTime: Bool, year: Int, year_length: Double, numberOfMonths: Int, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], month: Int, precise_month: Int, leap_month: Int, day: Int, sunTimes: [Date?], moonTimes: [Date?], startHour: Date, endHour: Date, hourNames: [String], hour_string: String, quarter_string: String) { + private init(compact: Bool, time: Date, calendar: Calendar, location: CGPoint?, globalMonth: Bool, apparentTime: Bool, largeHour: Bool, year: Int, year_length: Double, numberOfMonths: Int, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], month: Int, precise_month: Int, leap_month: Int, day: Int, sunTimes: [Date?], moonTimes: [Date?], startHour: Date, endHour: Date, hourNames: [NamedHour], hour_string: String, quarter_string: String) { self._compact = compact self._time = time self._calendar = calendar self._location = location self._globalMonth = globalMonth self._apparentTime = apparentTime + self._largeHour = largeHour self._year = year self._year_length = year_length self._numberOfMonths = numberOfMonths @@ -597,7 +617,7 @@ extension Array { } var copy: ChineseCalendar { - ChineseCalendar(compact: _compact, time: _time, calendar: _calendar, location: _location, globalMonth: _globalMonth, apparentTime: _apparentTime, year: _year, year_length: _year_length, numberOfMonths: _numberOfMonths, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, month: _month, precise_month: _precise_month, leap_month: _leap_month, day: _day, sunTimes: _sunTimes, moonTimes: _moonTimes, startHour: _startHour, endHour: _endHour, hourNames: _hourNames, hour_string: _hour_string, quarter_string: _quarter_string) + ChineseCalendar(compact: _compact, time: _time, calendar: _calendar, location: _location, globalMonth: _globalMonth, apparentTime: _apparentTime, largeHour: _largeHour, year: _year, year_length: _year_length, numberOfMonths: _numberOfMonths, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, month: _month, precise_month: _precise_month, leap_month: _leap_month, day: _day, sunTimes: _sunTimes, moonTimes: _moonTimes, startHour: _startHour, endHour: _endHour, hourNames: _hourNames, hour_string: _hour_string, quarter_string: _quarter_string) } private func updateYear() { @@ -721,23 +741,68 @@ extension Array { let startOfNextDay = startOfNextDay var tempStartHour: Date? var tempEndHour: Date? - var hour = startOfDay + var hour = if apparentTime { + startOfDay - 2 * startOfDay.distance(to: startOfNextDay) / 24 + } else { + _calendar.date(byAdding: .hour, value: -2, to: startOfDay)! + } + let end = if apparentTime { + startOfNextDay + 2 * startOfDay.distance(to: startOfNextDay) / 24 + } else { + _calendar.date(byAdding: .hour, value: 2, to: startOfNextDay)! + } _hourNames = [] - while hour < startOfNextDay - 1 { - let hourIndex: Int - if apparentTime { - hourIndex = Int(round(startOfDay.distance(to: hour) / startOfDay.distance(to: startOfNextDay) * 24)) + var prevHourName = "" + var prevOffsetHourName = "" + var prevLongName = "" + while hour < end { + let hourIndex = if apparentTime { + Int(round(startOfDay.distance(to: hour) / startOfDay.distance(to: startOfNextDay) * 24)) } else { - hourIndex = _calendar.component(.hour, from: hour) + _calendar.component(.hour, from: hour) + } + let hourName = Self.terrestrial_branches[(hourIndex /% 2) %% 12] + let offsetHourName = Self.terrestrial_branches[((hourIndex + 1) /% 2) %% 12] + if !_largeHour || hourName != prevHourName { + let shortName = if hourName != prevHourName { + ChineseCalendar.terrestrial_branches[(hourIndex /% 2) %% 12] + } else { + "" + } + let longName: String + if _largeHour { + if hourName != prevHourName { + longName = Self.terrestrial_branches[(hourIndex /% 2) %% 12] + } else { + longName = "" + } + } else { + let name = Self.terrestrial_branches[((hourIndex + 1) /% 2) %% 12] + Self.sub_hour_name[(hourIndex + 1) %% 2] + if name != prevLongName { + prevLongName = name + longName = name + } else { + longName = "" + } + } + _hourNames.append(NamedHour(hour: hour, shortName: shortName, longName: longName)) } - _hourNames.append((hourIndex %% 2) == 0 ? ChineseCalendar.terrestrial_branches[(hourIndex / 2) %% 12] : "") if hour <= _time { - _hour_string = Self.terrestrial_branches[((hourIndex + 1) / 2) %% 12] + Self.sub_hour_name[(hourIndex + 1) %% 2] + if _largeHour { + _hour_string = Self.terrestrial_branches[(hourIndex /% 2) %% 12] + } else { + _hour_string = Self.terrestrial_branches[((hourIndex + 1) /% 2) %% 12] + Self.sub_hour_name[(hourIndex + 1) %% 2] + } } - if (hourIndex %% 2) == 1, hour <= _time { + let changeOfHour = if _largeHour { + hourName != prevHourName + } else { + offsetHourName != prevOffsetHourName + } + if changeOfHour, hour <= _time { tempStartHour = hour } - if (hourIndex %% 2) == 1, hour > _time, tempEndHour == nil { + if changeOfHour, hour > _time, tempEndHour == nil { tempEndHour = hour } if apparentTime { @@ -745,28 +810,8 @@ extension Array { } else { hour = _calendar.date(byAdding: .hour, value: 1, to: hour)! } - } - if tempStartHour == nil { - if apparentTime { - tempStartHour = startOfDay - startOfDay.distance(to: startOfNextDay) / 24 - } else { - hour = _calendar.date(byAdding: .hour, value: -1, to: startOfDay)! - while (_calendar.component(.hour, from: hour) %% 2) == 0 { - hour = _calendar.date(byAdding: .hour, value: -1, to: hour)! - } - tempStartHour = hour - } - } - if tempEndHour == nil { - if apparentTime { - tempEndHour = startOfNextDay + startOfDay.distance(to: startOfNextDay) / 24 - } else { - hour = _calendar.date(byAdding: .hour, value: 1, to: startOfNextDay)! - while (_calendar.component(.hour, from: hour) %% 2) == 0 { - hour = _calendar.date(byAdding: .hour, value: 1, to: hour)! - } - tempEndHour = hour - } + prevHourName = hourName + prevOffsetHourName = offsetHourName } _startHour = tempStartHour! _endHour = tempEndHour! @@ -783,6 +828,7 @@ extension Array { _location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location _globalMonth = WatchLayout.shared.globalMonth _apparentTime = WatchLayout.shared.apparentTime + _largeHour = WatchLayout.shared.largeHour if (location == nil && oldLocation != nil) || (location != nil && oldLocation == nil) { updateYear() @@ -825,11 +871,19 @@ extension Array { } var timeString: String { - "\(_hour_string)\(_quarter_string)" + if _hour_string.count < 2 && _hour_string.count + _quarter_string.count < 5 { + "\(_hour_string)時\(_quarter_string)" + } else { + "\(_hour_string)\(_quarter_string)" + } } var hourString: String { - _hour_string + if _hour_string.count < 2 { + "\(_hour_string)時" + } else { + _hour_string + } } var quarterString: String { @@ -1012,17 +1066,10 @@ extension Array { } var hourTicks: Ticks { + let startOfDay = startOfDay + let startOfNextDay = startOfNextDay var ticks = Ticks() var hourDivides = [Double]() - var hour = startOfDay - while hour < startOfNextDay - 1 { - hourDivides.append(startOfDay.distance(to: hour) / startOfDay.distance(to: startOfNextDay)) - if apparentTime { - hour += startOfDay.distance(to: startOfNextDay) / 24 - } else { - hour = _calendar.date(byAdding: .hour, value: 1, to: hour)! - } - } var quarterTick = [Double]() var quarter = 0.0 while quarter < 1.0 { @@ -1031,16 +1078,28 @@ extension Array { } quarterTick = Array(Set(quarterTick).subtracting(hourDivides)).sorted() var hourNames = [Ticks.TickName]() - var hourStart = 0.0 - for i in 0..= startOfDay && namedHour.hour < startOfNextDay { + let dividePos = startOfDay.distance(to: namedHour.hour) / startOfDay.distance(to: startOfNextDay) + if !namedHour.longName.isEmpty { + hourDivides.append(dividePos) + } + if _largeHour || !namedHour.shortName.isEmpty { + if _largeHour { + hourStart = namedHour.hour + } + hourNames.append(Ticks.TickName( + pos: dividePos, + name: namedHour.shortName, + active: hourStart <= _time + )) + } else { + if !namedHour.longName.isEmpty { + hourStart = namedHour.hour + } + } } } ticks.majorTicks = hourDivides @@ -1053,30 +1112,20 @@ extension Array { var ticks = Ticks() var subHourTicks = Set() var majorTickNames = [String]() - var tickTime = startHour - var currentSmallHour = tickTime - _quarter_string = "" - while tickTime < endHour - 1 { - subHourTicks.insert(startHour.distance(to: tickTime) / startHour.distance(to: endHour)) - let hourOrder: Int - if apparentTime { - hourOrder = Int(round(startOfDay.distance(to: tickTime) / startOfDay.distance(to: startOfNextDay) * 24)) + 1 - } else { - hourOrder = _calendar.component(.hour, from: tickTime) + 1 - } - majorTickNames.append(Self.terrestrial_branches[(hourOrder / 2) %% 12] + Self.sub_hour_name[hourOrder %% 2]) - if tickTime <= _time { - currentSmallHour = tickTime - } - if apparentTime { - tickTime += startOfDay.distance(to: startOfNextDay) / 24 - } else { - tickTime = _calendar.date(byAdding: .hour, value: 1, to: tickTime)! + var currentSmallHour = startHour + for namedHour in _hourNames { + if !namedHour.longName.isEmpty && namedHour.hour >= startHour && namedHour.hour < endHour { + majorTickNames.append(namedHour.longName) + subHourTicks.insert(startHour.distance(to: namedHour.hour) / startHour.distance(to: endHour)) + if namedHour.hour <= _time { + currentSmallHour = namedHour.hour + } } } + let majorTicks = subHourTicks var majorTickCount = 0 - tickTime = startOfDay - 864 * 6 + var tickTime = startOfDay - 864 * 6 var currentSubhour = currentSmallHour while tickTime < endHour - 16 { if tickTime > startHour + 16 { diff --git a/Shared/DataModel/ThemeData.swift b/Shared/DataModel/ThemeData.swift index a2e7628..0772108 100644 --- a/Shared/DataModel/ThemeData.swift +++ b/Shared/DataModel/ThemeData.swift @@ -62,22 +62,27 @@ extension ThemeData: Identifiable, Hashable { static func latestVersion() -> Int { let deviceName = ThemeData.deviceName let predicate = #Predicate { data in - data.deviceName == deviceName + data.deviceName == deviceName && data.version != nil } - let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) - var version = 0 - do { - let records = try ThemeData.context.fetch(descriptor) - for record in records { - if !record.isNil { - version = record.version ?? version - break - } - } - } catch { - print(error.localizedDescription) + var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + descriptor.fetchLimit = 1 + let version = try? ThemeData.context.fetch(descriptor).first?.version + return version ?? 0 + } + static func experienced() -> Bool { + let predicate = #Predicate { data in + data.modifiedDate != nil + } + var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate)]) + let counts = try? ThemeData.context.fetchCount(descriptor) + descriptor.fetchLimit = 1 + let date = try? ThemeData.context.fetch(descriptor).first?.modifiedDate + + if let date = date, let counts = counts, counts > 1, date.distance(to: .now) > 3600 * 24 * 30 { + return true + } else { + return false } - return version } var isNil: Bool { diff --git a/Shared/Localizable.xcstrings b/Shared/Localizable.xcstrings index c79a0a8..a6b5100 100644 --- a/Shared/Localizable.xcstrings +++ b/Shared/Localizable.xcstrings @@ -189,6 +189,35 @@ } } }, + "不分初正" : { + "comment" : "Large hour setting: large", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "120 min" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "初正を区別しない" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "초정을 구분하지 않는다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不分初正" + } + } + } + }, "不得爲空,不得重名" : { "comment" : "no blank, no duplicate name", "extractionState" : "manual", @@ -352,7 +381,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "오성(五星)" + "value" : "오성" } }, "zh-Hans" : { @@ -426,31 +455,31 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "# Why create this Chinendar?\nChinendar is abbreviation of **Chinese Calendar**. Is the Chinese calendar still useful in daily life? In reality, it is practically useless, but as a form of cultural heritage, it can still serve as an exquisite decoration. Having seen too many traditional but outdated Chinese calendars, I had long wanted to create a modern one. That's why I made this.\nInspired by the design of watches, the months and years are displayed in a circular format similar to hours and minutes. With this design, the year, month, day, and hour can be easily read at a glance. Moreover, the calendar can also show the 24 solar terms, lunar phases, and leap months in an intuitive way.\n# What is the Chinese calendar?\nThe Chinese calendar is a traditional **lunar-solar calendar system** that is based on astronomical observations. It has a simple philosophy and unique beauty, but is challenging to calculate. Fortunately, modern technology has made the calculation much easier. In the past, people often wondered why the Chinese calendar date is irregular when comparing with Gregorian calendar dates, and do not follow a predictable pattern. However, after thorough study, the rules governing the Chinese calendar were found to be simple, yet the calculations they required were incredibly complex.\nIn the lunar-solar calendar system, the lunar part relates to the moon. The new moon, which occurs when the sun and the moon are at the same ecliptic longitude (and latitude during a solar eclipse), marks the first day of a month. This is an easily observable celestial phenomenon as the moon cannot be seen on that day. The full moon, on the other hand, is not as precisely observable since it can be slightly crescent or gibbous, making it difficult to determine the exact day. Hence, the ancient Chinese used the new moon to mark the beginning of month. Therefore, the first rule of the Chinese calendar system is: **the day of the new moon marks the first day of the month, and the period between two consecutive new moons is one month**.\nThe solar part refers to the sun in the Chinese calendar system. While the months are determined by the moon, but there needs a link between the lunar months and seasons. Therefore, it is the second rule that **winter solstices are always in the eleventh lunar month**. The winter solstice is the most important solar term, and it is easier to observe than the other solar terms, except for the summer solstice. The choice of the winter solstice as the starting point of the year might also be due to the fact that people have more free time during the winter season to make astronomical observations. Therefore, the winter solstice has become a significant holiday in ancient Chinese culture.\nOnce the 11th month is determined, the months between 2 consecutive winter solstices are named in sequence. Ideally, there should be 12 months between 2 winter solstices. However, the average year is 365.24 days long, which means there are 12.37 lunar months with an average of 29.53 days each. Sometimes there can be 13 months between two winter solstices, which requires an extra month, known as the leap month, to sync the lunar aspect to solar. To determine which month to add, the Chinese chose to use the 12 Even Solar Terms as a reference. There are 11 Even Solar Terms between two winter solstices, and if there are 13 lunar months, there will be a month without an Even Solar Term, which will be designated as the leap month. If there are 2 months without a solar term, only the first one will be designated as the leap month. Hence, the third and final rule: **if there are 13 new moons between two winter solstices, the first lunar month without an Even Solar Term repeats its preceding month, and is call Leap Month.**\n# Since it's an astronomical calendar, could the calculated dates differ due to different time zones?\nYes. \nThe Chinese calendar is defined based on celestial observations, and the ancient Chinese people mostly lived in East Asia, so their observations of celestial phenomena were similar. However, with global awareness, the problem of time zones arises. For example, if a new moon is observed in Beijing at 8 a.m. on the 23rd, that day is considered the first day of the month. But in New York, the new moon at 7 p.m. on the 22nd, which means 22nd is the first day. Therefore, countries that use the same calendar system, such as China, Japan, Korea, and Vietnam, may have slightly different dates for the first day of a month due to time differences.\nWhile the difference of month starts due to time difference is limited to 1 day, for a leap month, the difference can be much greater. The average interval between two solar terms is 30.44 days, and the average length of a lunar month is 29.53 days, which is not much different. Therefore, if there is no Even Solar Term within a lunar month, that month will be closely surrounded by Even Solar Terms. If there is a difference in the first day of the month, which month contains an Even Solar Term is a major question, and this difference can lead up to four months difference. While a difference of one day can be accepted, a difference of four months cannot be accepted.\nTherefore, there is a more precise way for leap month calculation: instead of counting EST in a month, count EST between 2 new moon moments. Although EST may fall on different dates due to time zones, the relative order of new moon and EST remains undisturbed. This is the \"**Finest Precision**\" option, which is not enabled by default and can be manually turned on.\n# What are Chinese Hour and sub-hour Quarter\nWhen you read \"3 quarters past noon\", what time is it exactly? What is the relationship between Hour and Quarter?. In fact, Hour and Quarter are different from what they mean in English.\nThe 12 words describing Hours originally referred to the twelve astrological signs used for year counting (associated with Jupiter's 11.83-year period). The oldest Hour counts was neither fixed at 12 a day, but can be wither 10 or 16. The length of each was also not fixed and was related to natural phenomena or daily activities, such as dawn, dusk, and breakfast time, and bedtime. The earliest precise time measurement was the water clock, on which Quarters were marked. **A day is divided into 100 Quarters with each Quarter equivalent to 14 minutes and 24 seconds**. However, counting up to 100 is difficult, so people used the 12 Hours in combination with Quarters to create the concept of a x Quarters past y Hour. The maximum Quarters after an Hour is limited, people could easily count them. This greatly improved readability.\nHowever, there was a problem: the interval between 2 Hours is 120 minutes, while the interval between Quarters is 14 minutes and 24 seconds. The former cannot divide the latter in whole. Therefore, only 4 Hours perfectly coincide with the Quarter while the others do not. Therefore, the duration of the first Quarter after each Hour is not the same. Those after the 1st, 4th, 7th and 10th Hour are complete Quarters, while those following other Hours are incomplete. The greatest common divisor of 60 minutes and 14.4 minutes is 2 minutes and 24 seconds, which is called a Minor Quarter. There are 6 Minor Quarters in a Quarter, and they were marked on the innermost ring on the clock.\nIt is worth noting that the ancient Chinese Hour refers to a moment, not a period. For example, the 1st Hour is the moment of 0:00, not the two-hour period from 23:00 the previous day to 1:00 the next day.\nAs for why Hour gradually had evolved to become time period, it is because with the advancement of timekeeping, an exquisite clock that displayed the Hour appeared some time in Song Dynasty. At noon, the 7th Hour appears in the center of the clock window, but the Hour did not just appear out of nowhere, instead, starting from 11:00, the 7th Hour sign enters in the corner of the window, at 12:00 reaches the center, and at 13:00 it disappears from view. This whole time period was then named after the Hour. In addition, each Hour was divided into two hours, with the first hour prefixed by Initial and the second hour by Proper.\n# Apparent Solar Time and Standard Time\nToday's timekeeping uses time zones. For example, when using UTC+8, noon is the noon at the meridian of 120°E longitude, and for places not exactly at 120°E, the true noon time is not 12:00. There is a **longitude time difference**. In addition, because the earth's orbit around the sun is not circular, it moves faster near perihelion and slower near aphelion, causing a slightly longer or shorter day than the average; this also affects the time of noon, which is called the **equation of time**.\nStandard time is the time commonly used in daily life, while apparent solar time is the time corrected for these two differences. The apparent solar noon is when the sun reaches its highest in the day, and the apparent solar midnight is when the sun is directly opposite behind the Earth. However, the noon and midnight in standard time have no astronomical significance.\n# What are the color marks on the Year Ring?\nIn the traditional Chinese practice, in addition to calculating days and time, the positions of the **five planets (Mercury, Venus, Mars, Jupiter and Saturn)** were also essential, for they are bright and moving. With modern astronomy, the positions of planets and moons can be calculated accurately. In particular, the positions of Jupiter and Saturn were also used for year counting in ancient China. Jupiter orbits the sun once every 11.86 years, which is approximately 12 years, so Jupiter's chronological year evolved into the Earthly Branches system of years. Saturn orbits the sun once every 29.5 years, and when combined with Jupiter, they form a cycle of 60 years, which is the famous Heavenly Stems and Earthly Branches system of years that is still used today.\nThere are **6 color marks on the Year Ring (5 planets and moon)**. To understand their position, first comes the fact that the 24 solar terms are both dates and ecliptic positions. For example, Spring Equinox is the position of the Sun on the ecliptic plane on the Spring Equinox day. If for example, Jupiter is at Spring Equinox means it's at the same direction as the Sun was on that day. The positions of Mercury and Venus are always near the Sun because their orbits are within the Earth's orbit. However, the positions of Mars, Jupiter and Saturn may not be near the Sun. The planets that are in front of the Sun (the transparent part on the Year Ring) rise before sunrise and set before sunset, while the planets that are behind the sun (the solid color part) rise after sunrise and set after sunset.\n# What are the color marks on the Month Ring?\nThere are generally 4 types: **New Moon, Full Moon, Odd Solar Term** and **Even Solar Term**, of which the exact colors can be changed in the settings. If the leap month is configured to \"Finest Precision\", then the Month Ring starts from the moment of the New Moon, and thus invalidates the need for New Moon mark, leaving only the other three color marks. The Full Moon marks the fullest moon moment in a month, you can observe for several months and tell whether the moon is the fullest on the 15th, 16th, or 17th. The Solar Term marks also correspond to the 24 solar terms on the Year Ring.\nWhen it is close to the time of a New Moon, Full Moon or Solar Term, the same color mark will also appear on the Day Ring and Hour Ring for accuracy. These four color marks appear on the **outer edge** of the Day and Hour rings.\n# Times of sunrise and moonrise\nThe times of sunrise and moonrise were crucial to ancient people and were essential in agriculture. So they are indispensable in the Chinese calendar.\nIf the location is enabled in settings, the local times of sunrise and moonrise will be displayed on the **inner edge** of Day Ring. When it is close to such a time, the same color mark will also appear on Hour Ring, also on the inner edge. There are 7 color marks in this category: **Sunrise, Noon, Sunset, Midnight, Moonrise, Moon at Meridian** and **Moonset**. The specific colors can also be changed in the settings. If the solar time setting is set to Apparent S Time, noon and midnight are no longer marked with color marks, since they are already apparent by time itself.\n# Terminologies\n**Solar Terms** are positions of Earth on its orbit. 4 solar terms are famous: 冬至 (Winter Solstice), 春分 (Spring Equinox), 夏至 (Summer Solstice), 秋分 (Autumn Equinox). Between each 2 of them, the 90° areas are further divided into 6 smaller divisions, with 5 more solar terms in each quadrant. This makes the total number of Solar Terms to 24, which are apart by 15° in the ecliptic plane.\n**Odd Solar Terms** are odd ones in solar terms, and thus are apart from each other by 30°. They do not include any of the equinoxes or solstices. The 12 of them are: 小寒, 立春, 驚蟄/啓蟄, 清明, 立夏, 芒種, 小暑, 立秋, 白露, 寒露, 立冬 and 大雪. 驚蟄/啓蟄 and 清明 were once Even Solar Terms before 85AC, then switched with 雨水 and 穀雨 respectively.\n**Even Solar Terms** are even ones in solar terms, also apart by 30°. They help determine the Leap Month in Chinese calendar (refer to \"What is the Chinese calendar\" section for details). The 12 of them are: 冬至 (Winter Solstice), 大寒, 雨水, 春分 (Spring Equinox), 穀雨, 小滿, 夏至 (Summer Solstice), 大暑, 處暑, 秋分 (Autumn Equinox), 霜降 and 小雪.\n**New Moon** is the moment when the sun and the moon are at the same ecliptic longitude (and latitude during a solar eclipse).\n**Full Moon** is the moment when the sun and the moon are opposite to each other over the Earth (lunar eclipse can happen on this moment).\n**Hour names** are 12 Earthly Branches in Chinese, apart from each other by 2 hours measured today. from 12am to before 12pm are: 子, 丑, 寅, 卯, 辰 and 巳, then from 12pm to before 12am next day are: 午, 未, 申, 酉, 戌 and 亥." + "value" : "# Why create this Chinendar?\nChinendar is abbreviation of **Chinese Calendar**. Is the Chinese calendar still useful in daily life? In reality, it is practically useless, but as a form of cultural heritage, it can still serve as an exquisite decoration. Having seen too many traditional but outdated Chinese calendars, I had long wanted to create a modern one. That's why I made this.\nInspired by the design of watches, the months and years are displayed in a circular format similar to hours and minutes. With this design, the year, month, day, and hour can be easily read at a glance. Moreover, the calendar can also show the 24 solar terms, lunar phases, and leap months in an intuitive way.\n# What is the Chinese calendar?\nThe Chinese calendar is a traditional **lunar-solar calendar system** that is based on astronomical observations. It has a simple philosophy and unique beauty, but is challenging to calculate. Fortunately, modern technology has made the calculation much easier. In the past, people often wondered why the Chinese calendar date is irregular when comparing with Gregorian calendar dates, and do not follow a predictable pattern. However, after thorough study, the rules governing the Chinese calendar were found to be simple, yet the calculations they required were incredibly complex.\nIn the lunar-solar calendar system, the lunar part relates to the moon. The new moon, which occurs when the sun and the moon are at the same ecliptic longitude (and latitude during a solar eclipse), marks the first day of a month. This is an easily observable celestial phenomenon as the moon cannot be seen on that day. The full moon, on the other hand, is not as precisely observable since it can be slightly crescent or gibbous, making it difficult to determine the exact day. Hence, the ancient Chinese used the new moon to mark the beginning of month. Therefore, the first rule of the Chinese calendar system is: **the day of the new moon marks the first day of the month, and the period between two consecutive new moons is one month**.\nThe solar part refers to the sun in the Chinese calendar system. While the months are determined by the moon, but there needs a link between the lunar months and seasons. Therefore, it is the second rule that **winter solstices are always in the eleventh lunar month**. The winter solstice is the most important solar term, and it is easier to observe than the other solar terms, except for the summer solstice. The choice of the winter solstice as the starting point of the year might also be due to the fact that people have more free time during the winter season to make astronomical observations. Therefore, the winter solstice has become a significant holiday in ancient Chinese culture.\nOnce the 11th month is determined, the months between 2 consecutive winter solstices are named in sequence. Ideally, there should be 12 months between 2 winter solstices. However, the average year is 365.24 days long, which means there are 12.37 lunar months with an average of 29.53 days each. Sometimes there can be 13 months between two winter solstices, which requires an extra month, known as the leap month, to sync the lunar aspect to solar. To determine which month to add, the Chinese chose to use the 12 Even Solar Terms as a reference. There are 11 Even Solar Terms between two winter solstices, and if there are 13 lunar months, there will be a month without an Even Solar Term, which will be designated as the leap month. If there are 2 months without a solar term, only the first one will be designated as the leap month. Hence, the third and final rule: **if there are 13 new moons between two winter solstices, the first lunar month without an Even Solar Term repeats its preceding month, and is call Leap Month.**\n# Since it's an astronomical calendar, could the calculated dates differ due to different time zones?\nYes. \nThe Chinese calendar is defined based on celestial observations, and the ancient Chinese people mostly lived in East Asia, so their observations of celestial phenomena were similar. However, with global awareness, the problem of time zones arises. For example, if a new moon is observed in Beijing at 8 a.m. on the 23rd, that day is considered the first day of the month. But in New York, the new moon at 7 p.m. on the 22nd, which means 22nd is the first day. Therefore, countries that use the same calendar system, such as China, Japan, Korea, and Vietnam, may have slightly different dates for the first day of a month due to time differences.\nWhile the difference of month starts due to time difference is limited to 1 day, for a leap month, the difference can be much greater. The average interval between two solar terms is 30.44 days, and the average length of a lunar month is 29.53 days, which is not much different. Therefore, if there is no Even Solar Term within a lunar month, that month will be closely surrounded by Even Solar Terms. If there is a difference in the first day of the month, which month contains an Even Solar Term is a major question, and this difference can lead up to four months difference. While a difference of one day can be accepted, a difference of four months cannot be accepted.\nTherefore, there is a more precise way for leap month calculation: instead of counting EST in a month, count EST between 2 new moon moments. Although EST may fall on different dates due to time zones, the relative order of new moon and EST remains undisturbed. This is the \"**Finest Precision**\" option, which is not enabled by default and can be manually turned on.\n# What are Chinese Hour and sub-hour Quarter\nWhen you read \"3 quarters past noon\", what time is it exactly? What is the relationship between Hour and Quarter?. In fact, Hour and Quarter are different from what they mean in English.\nThe 12 words describing Hours originally referred to the twelve astrological signs used for year counting (associated with Jupiter's 11.83-year period). The oldest Hour counts was neither fixed at 12 a day, but can be wither 10 or 16. The length of each was also not fixed and was related to natural phenomena or daily activities, such as dawn, dusk, and breakfast time, and bedtime. The earliest precise time measurement was the water clock, on which Quarters were marked. **A day is divided into 100 Quarters with each Quarter equivalent to 14 minutes and 24 seconds**. However, counting up to 100 is difficult, so people used the 12 Hours in combination with Quarters to create the concept of a x Quarters past y Hour. The maximum Quarters after an Hour is limited, people could easily count them. This greatly improved readability.\nHowever, there was a problem: the interval between 2 Hours is 120 minutes, while the interval between Quarters is 14 minutes and 24 seconds. The former cannot divide the latter in whole. Therefore, only 4 Hours perfectly coincide with the Quarter while the others do not. Therefore, the duration of the first Quarter after each Hour is not the same. Those after the 1st, 4th, 7th and 10th Hour are complete Quarters, while those following other Hours are incomplete. The greatest common divisor of 60 minutes and 14.4 minutes is 2 minutes and 24 seconds, which is called a Minor Quarter. There are 6 Minor Quarters in a Quarter, and they were marked on the innermost ring on the clock.\nIt is worth noting that the ancient Chinese Hour refers to a moment, not a period. For example, the 1st Hour is the moment of 0:00, not the two-hour period from 23:00 the previous day to 1:00 the next day. In general, there are 0-8 quarters after each Hour.\nAs for why Hour gradually had evolved to become time period, it is because with the advancement of timekeeping, an exquisite clock that displayed the Hour appeared some time in Song Dynasty. At noon, the 7th Hour appears in the center of the clock window, but the Hour did not just appear out of nowhere, instead, starting from 11:00, the 7th Hour sign enters in the corner of the window, at 12:00 reaches the center, and at 13:00 it disappears from view. This whole time period was then named after the Hour. In addition, each Hour was divided into two hours, with the first hour prefixed by Initial and the second hour by Proper, which is configurable in settings.\n# Apparent Solar Time and Standard Time\nToday's timekeeping uses time zones. For example, when using UTC+8, noon is the noon at the meridian of 120°E longitude, and for places not exactly at 120°E, the true noon time is not 12:00. There is a **longitude time difference**. In addition, because the earth's orbit around the sun is not circular, it moves faster near perihelion and slower near aphelion, causing a slightly longer or shorter day than the average; this also affects the time of noon, which is called the **equation of time**.\nStandard time is the time commonly used in daily life, while apparent solar time is the time corrected for these two differences. The apparent solar noon is when the sun reaches its highest in the day, and the apparent solar midnight is when the sun is directly opposite behind the Earth. However, the noon and midnight in standard time have no astronomical significance.\n# What are the color marks on the Year Ring?\nIn the traditional Chinese practice, in addition to calculating days and time, the positions of the **five planets (Mercury, Venus, Mars, Jupiter and Saturn)** were also essential, for they are bright and moving. With modern astronomy, the positions of planets and moons can be calculated accurately. In particular, the positions of Jupiter and Saturn were also used for year counting in ancient China. Jupiter orbits the sun once every 11.86 years, which is approximately 12 years, so Jupiter's chronological year evolved into the Earthly Branches system of years. Saturn orbits the sun once every 29.5 years, and when combined with Jupiter, they form a cycle of 60 years, which is the famous Heavenly Stems and Earthly Branches system of years that is still used today.\nThere are **6 color marks on the Year Ring (5 planets and moon)**. To understand their position, first comes the fact that the 24 solar terms are both dates and ecliptic positions. For example, Spring Equinox is the position of the Sun on the ecliptic plane on the Spring Equinox day. If for example, Jupiter is at Spring Equinox means it's at the same direction as the Sun was on that day. The positions of Mercury and Venus are always near the Sun because their orbits are within the Earth's orbit. However, the positions of Mars, Jupiter and Saturn may not be near the Sun. The planets that are in front of the Sun (the transparent part on the Year Ring) rise before sunrise and set before sunset, while the planets that are behind the sun (the solid color part) rise after sunrise and set after sunset.\n# What are the color marks on the Month Ring?\nThere are generally 4 types: **New Moon, Full Moon, Odd Solar Term** and **Even Solar Term**, of which the exact colors can be changed in the settings. If the leap month is configured to \"Finest Precision\", then the Month Ring starts from the moment of the New Moon, and thus invalidates the need for New Moon mark, leaving only the other three color marks. The Full Moon marks the fullest moon moment in a month, you can observe for several months and tell whether the moon is the fullest on the 15th, 16th, or 17th. The Solar Term marks also correspond to the 24 solar terms on the Year Ring.\nWhen it is close to the time of a New Moon, Full Moon or Solar Term, the same color mark will also appear on the Day Ring and Hour Ring for accuracy. These four color marks appear on the **outer edge** of the Day and Hour rings.\n# Times of sunrise and moonrise\nThe times of sunrise and moonrise were crucial to ancient people and were essential in agriculture. So they are indispensable in the Chinese calendar.\nIf the location is enabled in settings, the local times of sunrise and moonrise will be displayed on the **inner edge** of Day Ring. When it is close to such a time, the same color mark will also appear on Hour Ring, also on the inner edge. There are 7 color marks in this category: **Sunrise, Noon, Sunset, Midnight, Moonrise, Moon at Meridian** and **Moonset**. The specific colors can also be changed in the settings. If the solar time setting is set to Apparent S Time, noon and midnight are no longer marked with color marks, since they are already apparent by time itself.\n# Terminologies\n**Solar Terms** are positions of Earth on its orbit. 4 solar terms are famous: 冬至 (Winter Solstice), 春分 (Spring Equinox), 夏至 (Summer Solstice), 秋分 (Autumn Equinox). Between each 2 of them, the 90° areas are further divided into 6 smaller divisions, with 5 more solar terms in each quadrant. This makes the total number of Solar Terms to 24, which are apart by 15° in the ecliptic plane.\n**Odd Solar Terms** are odd ones in solar terms, and thus are apart from each other by 30°. They do not include any of the equinoxes or solstices. The 12 of them are: 小寒, 立春, 驚蟄/啓蟄, 清明, 立夏, 芒種, 小暑, 立秋, 白露, 寒露, 立冬 and 大雪. 驚蟄/啓蟄 and 清明 were once Even Solar Terms before 85AC, then switched with 雨水 and 穀雨 respectively.\n**Even Solar Terms** are even ones in solar terms, also apart by 30°. They help determine the Leap Month in Chinese calendar (refer to \"What is the Chinese calendar\" section for details). The 12 of them are: 冬至 (Winter Solstice), 大寒, 雨水, 春分 (Spring Equinox), 穀雨, 小滿, 夏至 (Summer Solstice), 大暑, 處暑, 秋分 (Autumn Equinox), 霜降 and 小雪.\n**New Moon** is the moment when the sun and the moon are at the same ecliptic longitude (and latitude during a solar eclipse).\n**Full Moon** is the moment when the sun and the moon are opposite to each other over the Earth (lunar eclipse can happen on this moment).\n**Hour names** are 12 Earthly Branches in Chinese, apart from each other by 2 hours measured today. from 12am to before 12pm are: 子, 丑, 寅, 卯, 辰 and 巳, then from 12pm to before 12am next day are: 午, 未, 申, 酉, 戌 and 亥." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "# なぜこの「華暦」を作成したのか?\n「華暦(かれき)」は**中華暦**を指し、漢暦としても知られています。現代の日常生活で華暦はまだ役に立つのでしょうか?実際にはほとんど役に立たないかもしれませんが、文化遺産としては、精巧な装飾としての価値があります。伝統的で時代遅れの華暦を数多く見てきた私は、現代的なものを作りたいと長年思っていました。それがこのプロジェクトを始めた理由です。\n腕時計の設計に触発されて、年と月を時と分のように円形で表示する設計を採用しました。この設計により、年、月、日、時を一目で簡単に読み取ることができます。さらに、この暦は24節気、月の位相、そして閏月も直感的に表示できます。\n# 華暦とは何か?\n華暦は天文観測に基づいた伝統的な**太陰太陽暦**です。朴素な哲学と独特の美しさを持ちながら、計算は複雑です。幸いなことに、現代技術により計算は格段に容易になりました。昔の人々はしばしば、華暦の日付がグレゴリオ暦と比較して不規則で予測不可能だと不思議に思っていましたが、徹底的に研究すると、華暦を支配するルールは単純であるものの、それに必要な計算は非常に複雑でした。\n太陰太陽暦では、太陰部分は月に関連しています。新月は、太陽と月が同じ黄経(太陽食の間は緯度も同じ)にある時に起こり、月の最初の日を示します。これは月がその日に見えないため、簡単に観察できる天文現象です。一方、満月は、わずかに三日月形か二十日月形であるため、正確な日を決定するのが難しく、厳密に観察できません。そのため、古代漢人は新月を月の始まりとして使いました。したがって、華暦システムの第一のルールは:**新月の日が月の最初の日であり、2つの連続する新月の間の期間が1ヶ月です**。\n太陽部分は華暦の太陽に関連しています。月は月によって決まりますが、月と季節の間に繋がりが必要です。したがって、第二の規則は**冬至は常に十一月にあります**。冬至は最も重要な節気であり、他の節気よりも観察しやすいですが、夏至を除いて。年の始まりの起点として冬至を選んだのは、冬の季節には天文観測をするための余暇が人々にはより多かったからかもしれません。そのため、冬至は古代中国文化で重要な祝日となりました。\n十一月が決まると、2つの連続する冬至の間の月は順番に命名されます。理想的には、2つの冬至の間には12ヶ月あります。しかし、平均年は365.24日で、平均して12.37太陰月があり、それぞれ平均29.53日です。時々2つの冬至の間に13ヶ月あり、太陰と太陽を同期させるために追加の月、いわゆる閏月が必要です。どの月を追加するかを決めるために、漢人は12個の中気を参照として使いました。2つの冬至の間に11個の中気がありますが、13太陰月がある場合、中気がない月があり、それが閏月として指定されます。2ヶ月連続して中気がない場合、最初の1ヶ月のみが閏月として指定されます。したがって、第三で最後の規則は:**2つの冬至の間に13回の新月がある場合、中気がない最初の太陰月が前の月を繰り返し、閏月と呼ばれます。**\n# 天文暦であるため、計算された日付は時間帯によって異なる可能性がありますか?\nはい。\n華暦は天体観測に基づいて定義されており、古代の漢人のほとんどは東アジアに住んでいたので、天体現象の観察は似ていました。しかし、世界な認識により、時間帯の問題が生じます。例えば、東京で23日の午前9時に新月が観察される場合、その日は月の最初の日と考えられます。しかし、ニューヨークでは22日の午後7時に新月があり、つまり22日が最初の日です。そのため、日本、中国、韓国、ベトナムなど、同じ暦システムを使用している国では、タイム差のために月の最初の日の日付がわずかに異なることがあります。\n月初めの違いは1日に限定されていますが、閏月の場合、差はもっと大きくなることがあります。2つの中気の間の平均間隔は30.44日で、太陰月の平均長さは29.53日で、これはそれほど異なりません。したがって、中気のない太陰月は、中気に囲まれています。月の最初の日が異なる場合、どの月が中気を含むかは重要な問題であり、この違いは最大で4ヶ月になる可能性があります。1日の違いは受け入れられるかもしれませんが、4ヶ月の違いは受け入れられません。\nそのため、閏月計算にはもっと正確な方法があります:月内の中気を数える代わりに、2つの新月の瞬間の間の中気を数えます。時間帯によって中気の日付は異なるかもしれませんが、新月と中気の相対的な順序は乱されません。これが「**瞬間の精度**」選択で、既定では有効になっておらず、手動でオンにすることができます。\n# 時と刻は何ですか?\n「午時三刻」と読むとき、それは具体的にどの時刻ですか?時と刻の関係は何ですか?実は、時と刻はまったく異なる計時システムです。\n時を表す12の言葉は元々、年の数え方に使われていた十二支(木星の11.83年周期に関連)を指します。最も古い時刻の数え方は1日に12回ではなく、10回や16回でした。また、それぞれの長さも固定されておらず、自然現象や日常活動、例えば夜明け、夕暮れ、朝食時間、就寝時間に関連していました。最初の正確な時間測定は水時計で、刻が印されていました。**1日は100刻に分けられ、各刻は14分24秒に相当します**。しかし、100まで数えるのは難しいため、人々は12の時と刻を組み合わせて、X刻過ぎY時の概念を作りました。時後の最大刻数は限定されているため、簡単に数えることができました。これにより、読みやすさが大幅に向上しました。\nしかし、問題がありました:2つの時の間隔は120分ですが、刻の間隔は14分24秒です。前者は後者を丸ごと分けることができません。そのため、完全に刻と一致する時は子、卯、午、酉の4つだけで、他の時は一致しません。したがって、各時の後の最初の刻の持続時間は同じではありません。子、卯、午、酉後のそれらは完全な刻ですが、他の時後のそれらは不完全です。60分と14.4分の最大公約数は2分24秒で、これは小刻と呼ばれます。1刻に小刻は6つあり、それらは時計の最内輪に記号されていました。\n一つはっきりさせておく必要がありますが、古代の時辰はある瞬間を指し、時間帯ではありません。子時は0:00のその瞬間を指し、前日の23:00から翌日の1:00までの2時間ではありません。子時三刻は子時を過ぎてから更に3刻が経過したことを意味し、子時の中の第三刻ではありません。\nなぜ時が時間期間に進化したのかというと、時計の進歩により、宋代のある時期に時を表示する精巧な時計が登場したからです。正午には、時計の窓の中心に午時が現れますが、時は突然現れるのではなく、11:00から窓の隅に午時の表示が入り、12:00に中心に達し、13:00には視界から消えます。この全時間期間がその後、時にちなんで名付けられました。さらに、各時は2時間に分けられ、最初の時間は「初」、二番目の時間は「正」と接頭されました。\n# 視太陽時與標準時間\n今日の時間計算は時間帯を使用します。例えば、UTC+9を使用するとき、正午は東経135°の子午線の正午ですが、正確に東経135°に位置していない場所では、真の正午時刻は12:00ではありません。これを**経度時間差**といいます。さらに、地球の太陽周りの軌道は円ではなく、近日点では速く、遠日点では遅く動くため、平均よりやや長いか短い日が生じ、これが正午の時間に影響します。これを**時差方程式**と呼びます。\n標準時間は日常生活で一般的に使用される時間であり、視太陽時はこれら二つの差を修正した時間です。視太陽の正午は、日中で太陽が最も高い位置に達した時、視太陽の真夜中は地球の真反対で太陽が直接反対に位置する時です。しかし、標準時間の正午や真夜中は天文学的な意味を持ちません。\n# 年輪上の色の記号は何ですか?\n伝統的な習慣では、日と時間の計算に加えて、**五惑星(水星/辰星、金星/太白、火星/螢惑、木星/歲星、土星/填星)** の位置も重要でした。これらは明るく動いています。現代の天文学では、惑星や月の位置を正確に計算できます。特に、古代中国では木星と土星の位置を年数えに使っていました。木星は約11.86年で太陽の周りを一周し、これがおおよそ12年であるため、木星の年は地支の年系統に進化しました。土星は太陽の周りを29.5年で一周し、木星と組み合わせると60年の周期、すなわち今日まで使用されている天干地支の年系統を形成します。\n年輪には**6色の記号(5惑星と月)が**あります。その位置を理解するには、24節気が日付とも黄道帯の位置ともなっている事実から始めます。例えば、春分は春分日の太陽の黄道帯上の位置です。例えば木星が春分にあるとは、その日に太陽があったのと同じ方向にあることを意味します。水星と金星の位置は常に太陽に近いですが、火星、木星、土星の位置は太陽から遠ざかることがあります。太陽の前方(年輪の透明部分)にある惑星は、日の出前に昇り、日没前に沈みますが、太陽の後方(固体色の部分)にある惑星は、日の出後に昇り、日没後に沈みます。\n# 月輪上の色の記号は何ですか?\n一般的に4種類あります:**新月、満月、節気**および**中気**で、具体的な色は設定で変更できます。うるう月が「最も正確」に設定されている場合、月輪は新月の瞬間から始まり、新月の記号の必要がなくなり、残りの3色の記号だけが残ります。満月は月の最も満ちた瞬間を示し、数ヶ月観察すると、月が15日、16日、または17日に最も満ちるかどうかを判断できます。節気の記号も年輪上の24節気に対応します。\n新月、満月、または節気に近づくと、同じ色の記号が正確さのために日輪と時輪の**外縁**にも現れます。\n# 日月出没の時間\n日の出と月の出の時間は古代人にとって重要で、農業に欠かせないものでした。したがって、華暦には欠かせません。\n設定で位置情報が有効になっている場合、日輪の**内縁**には日の出と月の出の地方時間が表示されます。そのような時間に近づくと、時輪の内縁にも同じ色の記号が現れます。このカテゴリーには7色の記号があります:**日出、日正/正午、日入/日没、夜中/子夜、月出、月正**および**月入/月没**。具体的な色も設定で変更できます。太陽時設定が視太陽時に設定されている場合、日中と夜中はもはや色の記号で示されません、なぜならそれらはすでに時間自体で明らかだからです。\n# 用語集\n**節気**は地球の軌道上の位置です。4つの節気が有名です:冬至、春分、夏至、秋分。それぞれの間の90°の領域はさらに6つの小区分に分けられ、各象限に5つの節気が追加されます。これにより、節気の総数は24になり、黄道帯上で15°間隔で分かれます。節気の奇数番目も同じように呼ばれます。これには春分や冬至などの分点や至点は含まれません。それらは12あります:小寒、立春、驚蟄/啓蟄、清明、立夏、芒種、小暑、立秋、白露、寒露、立冬、大雪。驚蟄/啓蟄と清明は85年前は中気であり、後に雨水と穀雨と入れ替わりました。\n**中気**は節気の偶数番目で、やはり30°間隔で分かれています。これらは華暦で閏月を決定するのに役立ちます(「華暦とは何か」の章を参照してください)。それらは12あります:冬至、大寒、雨水、春分、穀雨、小満、夏至、大暑、処暑、秋分、霜降、小雪。\n**朔**は太陽と月が同じ黄道経度(および日食時には緯度も)にある瞬間です。\n**望**は太陽と月が地球を挟んで互いに反対側に位置する瞬間です(この瞬間に月食が起こることがあります)。\n**時**は12支で、今日2時間ごとに分かれています。午前0時から午前12時前までは子、丑、寅、卯、辰、巳、そして午後12時から翌日午前0時前までは午、未、申、酉、戌、亥です。\n**五惑星**の古名は現在と異なります。辰星は現在の水星で、「星」とも呼ばれます;太白は現在の金星で、天空で最も明るい星であるため太白と呼ばれます;熒惑は現在の火星です;歳星は現在の木星で、太歳は歳星に関連していますが異なる概念です;填星、または鎮星とも呼ばれるのは、現在の土星です。\n**月名**: 1月から10月までは順番に一月から十月まで表示され、11月は冬月とされ、これは冬至がこの月に含まれるためです。12月は臘月とされ、古い時代に12月の戌の日に臘祭を行うためです。12個の月名がすべて二字であることを保つために、華暦では冬月、臘月を使用し、十一月、十二月は使用しません。\n\n" + "value" : "# なぜこの「華暦」を作成したのか?\n「華暦(かれき)」は**中華暦**を指し、漢暦としても知られています。現代の日常生活で華暦はまだ役に立つのでしょうか?実際にはほとんど役に立たないかもしれませんが、文化遺産としては、精巧な装飾としての価値があります。伝統的で時代遅れの華暦を数多く見てきた私は、現代的なものを作りたいと長年思っていました。それがこのプロジェクトを始めた理由です。\n腕時計の設計に触発されて、年と月を時と分のように円形で表示する設計を採用しました。この設計により、年、月、日、時を一目で簡単に読み取ることができます。さらに、この暦は24節気、月の位相、そして閏月も直感的に表示できます。\n# 華暦とは何か?\n華暦は天文観測に基づいた伝統的な**太陰太陽暦**です。朴素な哲学と独特の美しさを持ちながら、計算は複雑です。幸いなことに、現代技術により計算は格段に容易になりました。昔の人々はしばしば、華暦の日付がグレゴリオ暦と比較して不規則で予測不可能だと不思議に思っていましたが、徹底的に研究すると、華暦を支配するルールは単純であるものの、それに必要な計算は非常に複雑でした。\n太陰太陽暦では、太陰部分は月に関連しています。新月は、太陽と月が同じ黄経(太陽食の間は緯度も同じ)にある時に起こり、月の最初の日を示します。これは月がその日に見えないため、簡単に観察できる天文現象です。一方、満月は、わずかに三日月形か二十日月形であるため、正確な日を決定するのが難しく、厳密に観察できません。そのため、古代漢人は新月を月の始まりとして使いました。したがって、華暦システムの第一のルールは:**新月の日が月の最初の日であり、2つの連続する新月の間の期間が1ヶ月です**。\n太陽部分は華暦の太陽に関連しています。月は月によって決まりますが、月と季節の間に繋がりが必要です。したがって、第二の規則は**冬至は常に十一月にあります**。冬至は最も重要な節気であり、他の節気よりも観察しやすいですが、夏至を除いて。年の始まりの起点として冬至を選んだのは、冬の季節には天文観測をするための余暇が人々にはより多かったからかもしれません。そのため、冬至は古代中国文化で重要な祝日となりました。\n十一月が決まると、2つの連続する冬至の間の月は順番に命名されます。理想的には、2つの冬至の間には12ヶ月あります。しかし、平均年は365.24日で、平均して12.37太陰月があり、それぞれ平均29.53日です。時々2つの冬至の間に13ヶ月あり、太陰と太陽を同期させるために追加の月、いわゆる閏月が必要です。どの月を追加するかを決めるために、漢人は12個の中気を参照として使いました。2つの冬至の間に11個の中気がありますが、13太陰月がある場合、中気がない月があり、それが閏月として指定されます。2ヶ月連続して中気がない場合、最初の1ヶ月のみが閏月として指定されます。したがって、第三で最後の規則は:**2つの冬至の間に13回の新月がある場合、中気がない最初の太陰月が前の月を繰り返し、閏月と呼ばれます。**\n# 天文暦であるため、計算された日付は時間帯によって異なる可能性がありますか?\nはい。\n華暦は天体観測に基づいて定義されており、古代の漢人のほとんどは東アジアに住んでいたので、天体現象の観察は似ていました。しかし、世界な認識により、時間帯の問題が生じます。例えば、東京で23日の午前9時に新月が観察される場合、その日は月の最初の日と考えられます。しかし、ニューヨークでは22日の午後7時に新月があり、つまり22日が最初の日です。そのため、日本、中国、韓国、ベトナムなど、同じ暦システムを使用している国では、タイム差のために月の最初の日の日付がわずかに異なることがあります。\n月初めの違いは1日に限定されていますが、閏月の場合、差はもっと大きくなることがあります。2つの中気の間の平均間隔は30.44日で、太陰月の平均長さは29.53日で、これはそれほど異なりません。したがって、中気のない太陰月は、中気に囲まれています。月の最初の日が異なる場合、どの月が中気を含むかは重要な問題であり、この違いは最大で4ヶ月になる可能性があります。1日の違いは受け入れられるかもしれませんが、4ヶ月の違いは受け入れられません。\nそのため、閏月計算にはもっと正確な方法があります:月内の中気を数える代わりに、2つの新月の瞬間の間の中気を数えます。時間帯によって中気の日付は異なるかもしれませんが、新月と中気の相対的な順序は乱されません。これが「**瞬間の精度**」選択で、既定では有効になっておらず、手動でオンにすることができます。\n# 時と刻は何ですか?\n「午時三刻」と読むとき、それは具体的にどの時刻ですか?時と刻の関係は何ですか?実は、時と刻はまったく異なる計時システムです。\n時を表す12の言葉は元々、年の数え方に使われていた十二支(木星の11.83年周期に関連)を指します。最も古い時刻の数え方は1日に12回ではなく、10回や16回でした。また、それぞれの長さも固定されておらず、自然現象や日常活動、例えば夜明け、夕暮れ、朝食時間、就寝時間に関連していました。最初の正確な時間測定は水時計で、刻が印されていました。**1日は100刻に分けられ、各刻は14分24秒に相当します**。しかし、100まで数えるのは難しいため、人々は12の時と刻を組み合わせて、X刻過ぎY時の概念を作りました。時後の最大刻数は限定されているため、簡単に数えることができました。これにより、読みやすさが大幅に向上しました。\nしかし、問題がありました:2つの時の間隔は120分ですが、刻の間隔は14分24秒です。前者は後者を丸ごと分けることができません。そのため、完全に刻と一致する時は子、卯、午、酉の4つだけで、他の時は一致しません。したがって、各時の後の最初の刻の持続時間は同じではありません。子、卯、午、酉後のそれらは完全な刻ですが、他の時後のそれらは不完全です。60分と14.4分の最大公約数は2分24秒で、これは小刻と呼ばれます。1刻に小刻は6つあり、それらは時計の最内輪に記号されていました。\n一つはっきりさせておく必要がありますが、古代の時辰はある瞬間を指し、時間帯ではありません。子時は0:00のその瞬間を指し、前日の23:00から翌日の1:00までの2時間ではありません。子時三刻は子時を過ぎてから更に3刻が経過したことを意味し、子時の中の第三刻ではありません。各時辰の後に0(初)~8刻あります。\nなぜ時が時間期間に進化したのかというと、時計の進歩により、宋代のある時期に時を表示する精巧な時計が登場したからです。正午には、時計の窓の中心に午時が現れますが、時は突然現れるのではなく、11:00から窓の隅に午時の表示が入り、12:00に中心に達し、13:00には視界から消えます。この全時間期間がその後、時にちなんで名付けられました。さらに、各時は2時間に分けられ、最初の時間は「初」、二番目の時間は「正」と接頭され、設定で構成可能です。\n# 視太陽時與標準時間\n今日の時間計算は時間帯を使用します。例えば、UTC+9を使用するとき、正午は東経135°の子午線の正午ですが、正確に東経135°に位置していない場所では、真の正午時刻は12:00ではありません。これを**経度時間差**といいます。さらに、地球の太陽周りの軌道は円ではなく、近日点では速く、遠日点では遅く動くため、平均よりやや長いか短い日が生じ、これが正午の時間に影響します。これを**時差方程式**と呼びます。\n標準時間は日常生活で一般的に使用される時間であり、視太陽時はこれら二つの差を修正した時間です。視太陽の正午は、日中で太陽が最も高い位置に達した時、視太陽の真夜中は地球の真反対で太陽が直接反対に位置する時です。しかし、標準時間の正午や真夜中は天文学的な意味を持ちません。\n# 年輪上の色の記号は何ですか?\n伝統的な習慣では、日と時間の計算に加えて、**五惑星(水星/辰星、金星/太白、火星/螢惑、木星/歲星、土星/填星)** の位置も重要でした。これらは明るく動いています。現代の天文学では、惑星や月の位置を正確に計算できます。特に、古代中国では木星と土星の位置を年数えに使っていました。木星は約11.86年で太陽の周りを一周し、これがおおよそ12年であるため、木星の年は地支の年系統に進化しました。土星は太陽の周りを29.5年で一周し、木星と組み合わせると60年の周期、すなわち今日まで使用されている天干地支の年系統を形成します。\n年輪には**6色の記号(5惑星と月)が**あります。その位置を理解するには、24節気が日付とも黄道帯の位置ともなっている事実から始めます。例えば、春分は春分日の太陽の黄道帯上の位置です。例えば木星が春分にあるとは、その日に太陽があったのと同じ方向にあることを意味します。水星と金星の位置は常に太陽に近いですが、火星、木星、土星の位置は太陽から遠ざかることがあります。太陽の前方(年輪の透明部分)にある惑星は、日の出前に昇り、日没前に沈みますが、太陽の後方(固体色の部分)にある惑星は、日の出後に昇り、日没後に沈みます。\n# 月輪上の色の記号は何ですか?\n一般的に4種類あります:**新月、満月、節気**および**中気**で、具体的な色は設定で変更できます。うるう月が「最も正確」に設定されている場合、月輪は新月の瞬間から始まり、新月の記号の必要がなくなり、残りの3色の記号だけが残ります。満月は月の最も満ちた瞬間を示し、数ヶ月観察すると、月が15日、16日、または17日に最も満ちるかどうかを判断できます。節気の記号も年輪上の24節気に対応します。\n新月、満月、または節気に近づくと、同じ色の記号が正確さのために日輪と時輪の**外縁**にも現れます。\n# 日月出没の時間\n日の出と月の出の時間は古代人にとって重要で、農業に欠かせないものでした。したがって、華暦には欠かせません。\n設定で位置情報が有効になっている場合、日輪の**内縁**には日の出と月の出の地方時間が表示されます。そのような時間に近づくと、時輪の内縁にも同じ色の記号が現れます。このカテゴリーには7色の記号があります:**日出、日正/正午、日入/日没、夜中/子夜、月出、月正**および**月入/月没**。具体的な色も設定で変更できます。太陽時設定が視太陽時に設定されている場合、日中と夜中はもはや色の記号で示されません、なぜならそれらはすでに時間自体で明らかだからです。\n# 用語集\n**節気**は地球の軌道上の位置です。4つの節気が有名です:冬至、春分、夏至、秋分。それぞれの間の90°の領域はさらに6つの小区分に分けられ、各象限に5つの節気が追加されます。これにより、節気の総数は24になり、黄道帯上で15°間隔で分かれます。節気の奇数番目も同じように呼ばれます。これには春分や冬至などの分点や至点は含まれません。それらは12あります:小寒、立春、驚蟄/啓蟄、清明、立夏、芒種、小暑、立秋、白露、寒露、立冬、大雪。驚蟄/啓蟄と清明は85年前は中気であり、後に雨水と穀雨と入れ替わりました。\n**中気**は節気の偶数番目で、やはり30°間隔で分かれています。これらは華暦で閏月を決定するのに役立ちます(「華暦とは何か」の章を参照してください)。それらは12あります:冬至、大寒、雨水、春分、穀雨、小満、夏至、大暑、処暑、秋分、霜降、小雪。\n**朔**は太陽と月が同じ黄道経度(および日食時には緯度も)にある瞬間です。\n**望**は太陽と月が地球を挟んで互いに反対側に位置する瞬間です(この瞬間に月食が起こることがあります)。\n**時**は12支で、今日2時間ごとに分かれています。午前0時から午前12時前までは子、丑、寅、卯、辰、巳、そして午後12時から翌日午前0時前までは午、未、申、酉、戌、亥です。\n**五惑星**の古名は現在と異なります。辰星は現在の水星で、「星」とも呼ばれます;太白は現在の金星で、天空で最も明るい星であるため太白と呼ばれます;熒惑は現在の火星です;歳星は現在の木星で、太歳は歳星に関連していますが異なる概念です;填星、または鎮星とも呼ばれるのは、現在の土星です。\n**月名**: 1月から10月までは順番に一月から十月まで表示され、11月は冬月とされ、これは冬至がこの月に含まれるためです。12月は臘月とされ、古い時代に12月の戌の日に臘祭を行うためです。12個の月名がすべて二字であることを保つために、華暦では冬月、臘月を使用し、十一月、十二月は使用しません。\n\n" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "# 왜 이 화력(華曆)을 만들었는가?\n화력(華曆)은 **중화(中華)력** 즉 음력의 줄임말입니다. 화력(華曆)은 일상생활에서 아직 유용할까요? 실제로는 거의 쓸모가 없지만, 문화 유산의 한 형태로서, 아름다운 장식으로서의 역할을 할 수 있습니다. 전통적이지만 구식인 화력(華曆)을 너무 많이 보아온 나는, 현대적인 것을 만들고 싶었습니다. 그래서 이것을 만들게 되었습니다.\n시계 디자인에서 영감을 받아, 년과 월을 시간과 분처럼 원형으로 표시합니다. 이 디자인으로 해, 월, 일, 시를 한눈에 쉽게 읽을 수 있습니다. 게다가, 이 달력은 24절기, 달의 위상, 윤달을 직관적으로 보여줄 수 있습니다.\n# 화력(華曆)이란 무엇인가?\n화력(華曆)은 천문 관측에 기반한 전통적인 **음양력**입니다. 단순한 철학과 독특한 아름다움을 지녔지만, 계산이 어렵습니다. 다행히 현대 기술이 계산을 훨씬 쉽게 만들었습니다. 과거에 사람들은 그레고리력과 비교할 때 화력(華曆) 날짜가 불규칙하고 예측할 수 없다고 종종 궁금해했습니다. 하지만, 철저한 연구 후, 화력(華曆)을 지배하는 규칙은 단순하지만, 필요한 계산은 굉장히 복잡하다는 것을 발견했습니다.\n음력 부분은 달과 관련이 있습니다. 신월은 태양과 달이 같은 황도경도에 있을 때 발생합니다(일식 중에는 위도도 같음). 이는 달이 그날 볼 수 없는 쉽게 관찰할 수 있는 천문 현상입니다. 반면에 보름달은 정확히 관찰하기 어렵습니다. 그래서 고대인들은 월의 시작으로 신월을 사용했습니다. 따라서 음양력 체계의 첫 번째 규칙은: **신월의 날이 그 달의 첫째 날이고, 두 연속 신월 사이의 기간이 한 달입니다**.\n태양 부분은 음양력 체계에서 태양을 말합니다. 달로 월이 결정되지만, 태양 부분은 계절과 음력 월 사이의 연결고리가 필요합니다. 따라서 두 번째 규칙은 **동지는 항상 열한 번째 음력 월에 있습니다**. 동지는 가장 중요한 절기이며, 여름 동지를 제외하고는 다른 절기보다 관측하기 쉽습니다. 또한 동지를 연도의 시작점으로 선택한 이유는 겨울철에 사람들이 천문 관측을 할 시간이 더 많기 때문일 수도 있습니다. 따라서 동지는 고대 동아시아 문화에서 중요한 휴일이 되었습니다.\n11번째 달이 결정되면, 2번 연속 동지 사이의 월들은 순서대로 명명됩니다. 이상적으로는 2 동지 사이에 12개월이 있어야 합니다. 하지만, 평균 해는 365.24일이고, 평균 달은 29.53일로, 2 동지 사이에는 평균적으로 12.37개월이 있습니다. 때때로, 2 동지 사이에 13개월이 있을 수 있으며, 이는 음력을 태양에 맞추기 위해 추가되는 윤달이 필요합니다. 어떤 달을 추가할지 결정하기 위해, 고대인들은 12개의 중기(中氣)를 참조로 사용했습니다. 2 동지 사이에 11개의 중기(中氣)가 있고, 만약 13개의 음력 달이 있다면, 절기가 없는 달이 윤달로 지정됩니다. 만약 절기가 없는 달이 2개 있다면, 첫 번째 달만 윤달로 지정됩니다. 따라서 세 번째이자 마지막 규칙: **두 동지 사이에 13개의 신월이 있으면, 중기(中氣)가 없는 첫 번째 음력 달은 그 앞 달을 반복하며, 윤달이라고 합니다**.\n# 천문력인데, 시간대에 따라 계산된 날짜가 달라질 수 있나요?\n예.\n화력(華曆)은 천문 관측에 기반을 두고 있으며, 고대인들은 대부분 동아시아에 살았기 때문에, 천문 현상에 대한 관측이 비슷했습니다. 하지만, 글로벌 인식으로 인해, 시간대 문제가 발생합니다. 예를 들어, 23일 오전 9시에 서울에서 신월이 관측되면, 그 날은 그 달의 첫날로 간주됩니다. 그러나 뉴욕에서는 22일 오후 7시에 신월이 있으므로, 22일이 첫날이 됩니다. 따라서 같은 달력 체계를 사용하는 국가들, 예를 들어 한국, 중국, 일본, 베트남은 시간 차이로 인해 달의 첫날 날짜가 약간 다를 수 있습니다.\n달의 시작에 대한 시간 차이는 1일로 제한되지만, 윤달에 대한 차이는 훨씬 클 수 있습니다. 2절기 사이의 평균 간격은 30.44일이고, 음력 달의 평균 길이는 29.53일로 크게 다르지 않습니다. 따라서, 음력 달 내에 중기(中氣)가 없다면, 그 달은 중기(中氣)에 둘러싸여 있을 것입니다. 달의 첫날에 차이가 있는 경우, 어떤 달에 중기(中氣)가 포함되는지는 중요한 질문이며, 이 차이는 최대 4개월에 이를 수 있습니다. 1일 차이는 받아들일 수 있지만, 4개월 차이는 받아들일 수 없습니다.\n따라서 윤달 계산에는 더 정확한 방법이 있습니다: 달에 중기(中氣)를 세는 대신, 2개의 신월 사이에 중기(中氣)를 세십시오. 시간대에 따라 중기(中氣)의 날짜가 다를 수 있지만, 신월과 중기(中氣)의 상대적인 순서는 바뀌지 않습니다. 이것이 \"**순간의 정밀도**\" 옵션으로, 기본적으로 활성화되지 않으며 수동으로 켤 수 있습니다.\n# 화력(華曆)의 시(時)와 각(刻)이란?\n정오로부터 \"오시삼각(午時三刻)\" 시간이 정확히 몇 시인지, 시(時)와 각(刻)의 관계는 무엇일까요? 사실 시(時)와 각(刻)은 전혀 다른 두 가지 시간 계산 방법입니다.\n시를 나타내는 12개의 단어는 원래 목성의 11.83년 주기와 관련된 12지지(地支)를 연도 계산에 사용된 점성술적 기호를 의미했습니다. 가장 오래된 시계는 하루에 12시(時)로 고정되어 있지 않았으며 10시(時)나 16시(時)일 수도 있었습니다. 시(時)의 길이도 고정되지 않았으며 새벽, 황혼, 아침 식사 시간, 취침 시간과 같은 자연 현상이나 일상 활동과 관련되었습니다. 가장 정밀한 시간 측정은 물시계였으며, 각(刻)은 물시계에 표시되었습니다. **하루는 100각(刻)으로 나뉘며, 각(刻)은 14분 24초와 동일합니다.** 하지만 100까지 세기는 어려웠기 때문에 사람들은 시(時)와 각(刻)을 결합하여 \"X시Y각\" 지난 시간의 개념을 만들었습니다. 각(刻) 시(時) 후의 최대 각수는 제한되어 있어 사람들이 쉽게 셀 수 있었습니다. 이는 가독성을 크게 향상시켰습니다.\n그러나 문제가 있었습니다: 2시(時) 사이의 간격은 120분이고, 2각(刻) 사이의 간격은 14분 24초입니다. 전자는 후자를 정수로 나눌 수 없습니다. 따라서 4개의 시(時)만 각(刻)과 완벽하게 일치하고 나머지는 그렇지 않습니다. 따라서 각 시 후의 첫 각의 지속 시간은 같지 않습니다. 자(子), 묘(卯), 오(午), 유(酉)시 후의 각(刻)은 완전한 각(刻)이지만, 다른 시(時) 후의 각은 완전하지 않습니다. 60분과 14.4분의 최대 공약수는 2분 24초로, 이를 소각(小刻)이라고 합니다. 한 각(刻)에는 6개의 소각(小刻)이 있으며, 시계의 가장 안쪽 고리에 표시되었습니다.\n확실히 해야 할 한 가지는 고대의 시(時)는 기간이 아닌 순간을 의미한다는 점에 유의해야 합니다. 자시(子時)는 바로 0:00의 그 순간을 말하며, 전날 23:00부터 다음 날 1:00까지의 두 시간이 아닙니다. 자시삼각(子時三刻)은 23:00~1:00 사이에 3번째 틱이 아니라 0:00 이후에 3틱이 있다는 뜻입니다.\n시(時)가 점차 시간 단위로 발전한 이유는 시간 측정 기술의 발전으로 송나라 시기 어느 때인가 시를 표시하는 정교한 시계가 등장했기 때문입니다. 정오에는 오시(午時)가 시계 창의 중앙에 나타납니다. 그러나 시는 갑자기 나타나는 것이 아니라 11:00부터 시작하여 오시(午時) 표시가 창의 모서리에서 들어와 12:00에 중앙에 도달하고 13:00에는 시야에서 사라집니다. 이 전체 시간 구간은 그 시(時)를 따서 명명되었습니다. 또한, 각 시(時)는 두 시간으로 나뉘어 첫 시간은 \"초(初)\"로, 두 번째 시간은 \"정(正)\"로 나뉩니다.\n# 태양시와 표준시\n오늘날의 시간 측정은 시간대를 사용합니다. 예를 들어, UTC+9을 사용할 때 정오는 135°E 경도의 정오입니다. 135°E에 정확히 위치하지 않은 곳의 진정한 정오 시간은 12:00이 아닙니다. **경도 시간 차이**가 있습니다. 또한, 지구가 태양 주변을 돌 때의 궤도가 원이 아니어서, 지구는 근일점에서 빠르게 움직이고 원일점에서 느리게 움직여서, 평균보다 약간 길거나 짧은 날이 생기게 되는데, 이것도 정오의 시간에 영향을 줍니다. 이를 **시차 방정식**이라고 합니다.\n표준시는 일상 생활에서 흔히 사용되는 시간이며, 태양시는 이 두 차이를 수정한 시간입니다. 태양 정오는 태양이 하루 중 가장 높이 올라갈 때이며, 태양 자정은 태양이 지구 뒤에서 정확히 반대편에 있을 때입니다. 그러나 표준시의 정오와 자정은 천문학적인 의미가 없습니다.\n# 년륜(年輪)의 색상 표시는 무엇인가요?\n고대 전통에서는 날짜와 시간을 계산할 뿐만 아니라 **다섯 행성(수성, 금성, 화성, 목성, 토성)의** 위치도 중요했습니다. 이들은 밝고 움직이기 때문입니다. 현대 천문학으로 행성과 달의 위치를 정확히 계산할 수 있습니다. 특히 고대에서는 목성과 토성의 위치를 연대기에 사용했습니다. 목성은 11.86년마다 한 번씩 태양 주변을 돌며, 이는 대략 12년이므로 목성의 연대기는 지지(地支) 연도 체계로 발전했습니다. 토성은 29.5년마다 한 번씩 태양 주변을 돌며, 목성과 결합하여 60년의 주기를 형성하는데, 이것이 오늘날까지 사용되고 있는 유명한 천간지지(天干地支) 연도 체계입니다.\n년륜(年輪)에는 **6개의 색상 표시(5개 행성과 달)가** 있습니다. 그 위치를 이해하려면, 24절기가 날짜이자 황도의 위치라는 사실부터 시작해야 합니다. 예를 들어, 춘분은 춘분일에 태양이 황도면상의 위치입니다. 예를 들어, 목성이 춘분에 있다는 것은 그날 태양이 있던 같은 방향에 있다는 것을 의미합니다. 수성과 금성의 위치는 항상 태양 근처에 있습니다. 왜냐하면 그들의 궤도는 지구 궤도 내부에 있기 때문입니다. 그러나 화성, 목성, 토성의 위치는 태양 근처에 있지 않을 수 있습니다. 태양 앞에 있는 행성들(년륜(年輪)의 투명 부분)은 일출 전에 뜨고 일몰 전에 지며, 태양 뒤에 있는 행성들(고체 색 부분)은 일출 후에 뜨고 일몰 후에 집니다.\n# 월륜(月輪)의 색상 표시는 무엇인가요?\n일반적으로 4가지 유형이 있습니다: **신월, 만월, 절기** 및 **중기(中氣)** 로, 정확한 색상은 설정에서 변경할 수 있습니다. 윤달이 \"순간의 정밀도\"으로 설정되면, 월륜(月輪)은 신월 순간부터 시작되므로, 신월 표시의 필요성이 없어져, 다른 세 가지 색깔 표시만 남습니다. 보름은 한 달 중 달이 가장 찬란한 순간을 표시하며, 몇 달 동안 관찰하여 보름이 15, 16, 또는 17일에 있는지 알 수 있습니다. 절기 표시도 년륜(年輪)의의 24절기와 일치합니다.\n신월, 만월 또는 절기 시간에 가까워지면, 같은 색깔의 표시도 일륜(日輪)과 시륜(時輪)의 **외곽**에 나타납니다. 이 네 가지 색깔 표시는 일륜(日輪)과 시륜(時輪)의 외곽에 나타납니다.\n# 일월출몰(日月出沒) 시간\n일출, 일몰, 월출 및 월몰 시간은 고대 사람들에게 중요했으며 농업에서 필수적이었습니다. 따라서 이는 화력(華曆)에 없어서는 안 될 부분입니다.\n설정에서 위치가 활성화되면, 일륜(日輪)의 **내부 가장자리**에 있는 지역의 일월출몰 시간이 표시됩니다. 이러한 시간에 가까워지면, 시륜(時輪)의 내부 가장자리에도 같은 색깔의 표시가 나타납니다. 이 범주에는 7가지 색깔 표시가 있습니다: **일출, 정오, 일몰, 자정, 월출, 월정(月正)** 및 **월몰**. 특정 색상도 설정에서 변경할 수 있습니다. 태양시 설정이 실제 태양시로 설정되면, 정오와 자정은 색깔 표시로 표시되지 않습니다. 왜냐하면 그 시간 자체로 이미 명확하기 때문입니다.\n# 용어 설명\n**절기**는 지구가 궤도상의 위치입니다. 4개의 절기는 유명합니다: 동지, 춘분, 하지, 추분. 각각 사이의 90° 영역은 6개의 더 작은 구분으로 나뉘며, 각 사분면에 5개의 추가 절기가 있습니다. 총 24개의 절기가 있으며, 황도면에서 15°씩 떨어져 있습니다. 절기의 홀수 번째도 마찬가지로 불립니다. 12개는 다음과 같습니다: 소한(小寒), 입춘(立春), 경칩(驚蟄), 청명(清明), 입하(立夏), 망종(芒種), 소서(小暑), 입추(立秋), 백로(白露), 한로(寒露), 입동(立冬) 및 대설(大雪). 경칩(驚蟄)와 청명(清明)은 원래 85AC 이전에는 짝수 절기였으나, 雨水와 穀雨와 교체되었습니다.\n**중기(中氣)** 는 절기 중 짝수이며, 서로 30°씩 떨어져 있습니다. 이들은 화력(華曆)에서 윤달을 결정하는 데 도움이 됩니다(자세한 내용은 \"화력(華曆)이란 무엇인가\" 섹션을 참조하세요). 12개는 다음과 같습니다: 동지, 대한(大寒), 우수(雨水), 춘분, 곡우(穀雨), 소만(小滿), 하지, 대서(大暑), 처서(處暑), 추분, 상강(霜降) 및 소설(小雪).\n**신월**은 태양과 달이 같은 황도 경도(태양일식 중에는 위도)에 위치할 때의 순간입니다.\n**만월**은 태양과 달이 지구를 사이에 두고 서로 반대편에 위치할 때의 순간입니다(이때 달의 일식이 일어날 수 있습니다).\n**시(時) 이름**은 12지지(地支)로, 오늘날 2시간마다 나뉘어져 있습니다. 12am부터 12pm 전까지는: 자(子), 축(丑), 인(寅), 묘(卯), 진(辰), 사(巳)이고, 12pm부터 다음 날 12am 전까지는: 오(午), 미(未), 신(申), 유(酉), 술(戌) 및 해(亥)입니다.\n**오성(五星)** 의 고명은 현재와 다릅니다. 진성(辰星)은 현재의 수성으로, '성'이라고도 불립니다; 태백(太白)은 현재의 금성으로, 하늘에서 가장 밝은 별이기 때문에 태백이라고 불립니다; 형혹(熒惑)은 현재의 화성입니다; 세성(歳星)은 현재의 목성으로, 태세(太歳)는 세성과 관련이 있지만 다른 개념입니다; 천성(填星) 또는 진성(鎮星)은 현재의 토성입니다.\n**월명**: 1월부터 10월까지는 순서대로 일월에서 십월까지 표기하고, 11월은 동월(冬月)로, 이는 동지가 이 달에 포함되기 때문입니다. 12월은 납월(臘月)로, 고대에 12월의 술일(戌日)에 납제(臘祭) 행사를 진행했기 때문입니다. 12개의 월명이 모두 두 글자가 되도록 유지하기 위해, 화력(華曆)은 동월(冬月), 납월(臘月)을 사용하고 11월, 12월은 사용하지 않습니다.\n\n" + "value" : "# 왜 이 화력(華曆)을 만들었는가?\n화력(華曆)은 **중화(中華)력** 즉 음력의 줄임말입니다. 화력(華曆)은 일상생활에서 아직 유용할까요? 실제로는 거의 쓸모가 없지만, 문화 유산의 한 형태로서, 아름다운 장식으로서의 역할을 할 수 있습니다. 전통적이지만 구식인 화력(華曆)을 너무 많이 보아온 나는, 현대적인 것을 만들고 싶었습니다. 그래서 이것을 만들게 되었습니다.\n시계 디자인에서 영감을 받아, 년과 월을 시간과 분처럼 원형으로 표시합니다. 이 디자인으로 해, 월, 일, 시를 한눈에 쉽게 읽을 수 있습니다. 게다가, 이 달력은 24절기, 달의 위상, 윤달을 직관적으로 보여줄 수 있습니다.\n# 화력(華曆)이란 무엇인가?\n화력(華曆)은 천문 관측에 기반한 전통적인 **음양력**입니다. 단순한 철학과 독특한 아름다움을 지녔지만, 계산이 어렵습니다. 다행히 현대 기술이 계산을 훨씬 쉽게 만들었습니다. 과거에 사람들은 그레고리력과 비교할 때 화력(華曆) 날짜가 불규칙하고 예측할 수 없다고 종종 궁금해했습니다. 하지만, 철저한 연구 후, 화력(華曆)을 지배하는 규칙은 단순하지만, 필요한 계산은 굉장히 복잡하다는 것을 발견했습니다.\n음력 부분은 달과 관련이 있습니다. 신월은 태양과 달이 같은 황도경도에 있을 때 발생합니다(일식 중에는 위도도 같음). 이는 달이 그날 볼 수 없는 쉽게 관찰할 수 있는 천문 현상입니다. 반면에 보름달은 정확히 관찰하기 어렵습니다. 그래서 고대인들은 월의 시작으로 신월을 사용했습니다. 따라서 음양력 체계의 첫 번째 규칙은: **신월의 날이 그 달의 첫째 날이고, 두 연속 신월 사이의 기간이 한 달입니다**.\n태양 부분은 음양력 체계에서 태양을 말합니다. 달로 월이 결정되지만, 태양 부분은 계절과 음력 월 사이의 연결고리가 필요합니다. 따라서 두 번째 규칙은 **동지는 항상 열한 번째 음력 월에 있습니다**. 동지는 가장 중요한 절기이며, 여름 동지를 제외하고는 다른 절기보다 관측하기 쉽습니다. 또한 동지를 연도의 시작점으로 선택한 이유는 겨울철에 사람들이 천문 관측을 할 시간이 더 많기 때문일 수도 있습니다. 따라서 동지는 고대 동아시아 문화에서 중요한 휴일이 되었습니다.\n11번째 달이 결정되면, 2번 연속 동지 사이의 월들은 순서대로 명명됩니다. 이상적으로는 2 동지 사이에 12개월이 있어야 합니다. 하지만, 평균 해는 365.24일이고, 평균 달은 29.53일로, 2 동지 사이에는 평균적으로 12.37개월이 있습니다. 때때로, 2 동지 사이에 13개월이 있을 수 있으며, 이는 음력을 태양에 맞추기 위해 추가되는 윤달이 필요합니다. 어떤 달을 추가할지 결정하기 위해, 고대인들은 12개의 중기(中氣)를 참조로 사용했습니다. 2 동지 사이에 11개의 중기(中氣)가 있고, 만약 13개의 음력 달이 있다면, 절기가 없는 달이 윤달로 지정됩니다. 만약 절기가 없는 달이 2개 있다면, 첫 번째 달만 윤달로 지정됩니다. 따라서 세 번째이자 마지막 규칙: **두 동지 사이에 13개의 신월이 있으면, 중기(中氣)가 없는 첫 번째 음력 달은 그 앞 달을 반복하며, 윤달이라고 합니다**.\n# 천문력인데, 시간대에 따라 계산된 날짜가 달라질 수 있나요?\n예.\n화력(華曆)은 천문 관측에 기반을 두고 있으며, 고대인들은 대부분 동아시아에 살았기 때문에, 천문 현상에 대한 관측이 비슷했습니다. 하지만, 글로벌 인식으로 인해, 시간대 문제가 발생합니다. 예를 들어, 23일 오전 9시에 서울에서 신월이 관측되면, 그 날은 그 달의 첫날로 간주됩니다. 그러나 뉴욕에서는 22일 오후 7시에 신월이 있으므로, 22일이 첫날이 됩니다. 따라서 같은 달력 체계를 사용하는 국가들, 예를 들어 한국, 중국, 일본, 베트남은 시간 차이로 인해 달의 첫날 날짜가 약간 다를 수 있습니다.\n달의 시작에 대한 시간 차이는 1일로 제한되지만, 윤달에 대한 차이는 훨씬 클 수 있습니다. 2절기 사이의 평균 간격은 30.44일이고, 음력 달의 평균 길이는 29.53일로 크게 다르지 않습니다. 따라서, 음력 달 내에 중기(中氣)가 없다면, 그 달은 중기(中氣)에 둘러싸여 있을 것입니다. 달의 첫날에 차이가 있는 경우, 어떤 달에 중기(中氣)가 포함되는지는 중요한 질문이며, 이 차이는 최대 4개월에 이를 수 있습니다. 1일 차이는 받아들일 수 있지만, 4개월 차이는 받아들일 수 없습니다.\n따라서 윤달 계산에는 더 정확한 방법이 있습니다: 달에 중기(中氣)를 세는 대신, 2개의 신월 사이에 중기(中氣)를 세십시오. 시간대에 따라 중기(中氣)의 날짜가 다를 수 있지만, 신월과 중기(中氣)의 상대적인 순서는 바뀌지 않습니다. 이것이 \"**순간의 정밀도**\" 옵션으로, 기본적으로 활성화되지 않으며 수동으로 켤 수 있습니다.\n# 화력(華曆)의 시(時)와 각(刻)이란?\n정오로부터 \"오시삼각(午時三刻)\" 시간이 정확히 몇 시인지, 시(時)와 각(刻)의 관계는 무엇일까요? 사실 시(時)와 각(刻)은 전혀 다른 두 가지 시간 계산 방법입니다.\n시를 나타내는 12개의 단어는 원래 목성의 11.83년 주기와 관련된 12지지(地支)를 연도 계산에 사용된 점성술적 기호를 의미했습니다. 가장 오래된 시계는 하루에 12시(時)로 고정되어 있지 않았으며 10시(時)나 16시(時)일 수도 있었습니다. 시(時)의 길이도 고정되지 않았으며 새벽, 황혼, 아침 식사 시간, 취침 시간과 같은 자연 현상이나 일상 활동과 관련되었습니다. 가장 정밀한 시간 측정은 물시계였으며, 각(刻)은 물시계에 표시되었습니다. **하루는 100각(刻)으로 나뉘며, 각(刻)은 14분 24초와 동일합니다.** 하지만 100까지 세기는 어려웠기 때문에 사람들은 시(時)와 각(刻)을 결합하여 \"X시Y각\" 지난 시간의 개념을 만들었습니다. 각(刻) 시(時) 후의 최대 각수는 제한되어 있어 사람들이 쉽게 셀 수 있었습니다. 이는 가독성을 크게 향상시켰습니다.\n그러나 문제가 있었습니다: 2시(時) 사이의 간격은 120분이고, 2각(刻) 사이의 간격은 14분 24초입니다. 전자는 후자를 정수로 나눌 수 없습니다. 따라서 4개의 시(時)만 각(刻)과 완벽하게 일치하고 나머지는 그렇지 않습니다. 따라서 각 시 후의 첫 각의 지속 시간은 같지 않습니다. 자(子), 묘(卯), 오(午), 유(酉)시 후의 각(刻)은 완전한 각(刻)이지만, 다른 시(時) 후의 각은 완전하지 않습니다. 60분과 14.4분의 최대 공약수는 2분 24초로, 이를 소각(小刻)이라고 합니다. 한 각(刻)에는 6개의 소각(小刻)이 있으며, 시계의 가장 안쪽 고리에 표시되었습니다.\n확실히 해야 할 한 가지는 고대의 시(時)는 기간이 아닌 순간을 의미한다는 점에 유의해야 합니다. 자시(子時)는 바로 0:00의 그 순간을 말하며, 전날 23:00부터 다음 날 1:00까지의 두 시간이 아닙니다. 자시삼각(子時三刻)은 23:00~1:00 사이에 3번째 틱이 아니라 0:00 이후에 3각(刻)이 있다는 뜻입니다.\n시(時)가 점차 시간 단위로 발전한 이유는 시간 측정 기술의 발전으로 송나라 시기 어느 때인가 시를 표시하는 정교한 시계가 등장했기 때문입니다. 정오에는 오시(午時)가 시계 창의 중앙에 나타납니다. 그러나 시는 갑자기 나타나는 것이 아니라 11:00부터 시작하여 오시(午時) 표시가 창의 모서리에서 들어와 12:00에 중앙에 도달하고 13:00에는 시야에서 사라집니다. 이 전체 시간 구간은 그 시(時)를 따서 명명되었습니다. 또한, 각 시(時)는 두 시간으로 나뉘어 첫 시간은 \"초(初)\"로, 두 번째 시간은 \"정(正)\"로 나뉘며 이는 설정에서 구성할 수 있습니다.\n# 태양시와 표준시\n오늘날의 시간 측정은 시간대를 사용합니다. 예를 들어, UTC+9을 사용할 때 정오는 135°E 경도의 정오입니다. 135°E에 정확히 위치하지 않은 곳의 진정한 정오 시간은 12:00이 아닙니다. **경도 시간 차이**가 있습니다. 또한, 지구가 태양 주변을 돌 때의 궤도가 원이 아니어서, 지구는 근일점에서 빠르게 움직이고 원일점에서 느리게 움직여서, 평균보다 약간 길거나 짧은 날이 생기게 되는데, 이것도 정오의 시간에 영향을 줍니다. 이를 **시차 방정식**이라고 합니다.\n표준시는 일상 생활에서 흔히 사용되는 시간이며, 태양시는 이 두 차이를 수정한 시간입니다. 태양 정오는 태양이 하루 중 가장 높이 올라갈 때이며, 태양 자정은 태양이 지구 뒤에서 정확히 반대편에 있을 때입니다. 그러나 표준시의 정오와 자정은 천문학적인 의미가 없습니다.\n# 년륜(年輪)의 색상 표시는 무엇인가요?\n고대 전통에서는 날짜와 시간을 계산할 뿐만 아니라 **다섯 행성(수성, 금성, 화성, 목성, 토성)의** 위치도 중요했습니다. 이들은 밝고 움직이기 때문입니다. 현대 천문학으로 행성과 달의 위치를 정확히 계산할 수 있습니다. 특히 고대에서는 목성과 토성의 위치를 연대기에 사용했습니다. 목성은 11.86년마다 한 번씩 태양 주변을 돌며, 이는 대략 12년이므로 목성의 연대기는 지지(地支) 연도 체계로 발전했습니다. 토성은 29.5년마다 한 번씩 태양 주변을 돌며, 목성과 결합하여 60년의 주기를 형성하는데, 이것이 오늘날까지 사용되고 있는 유명한 천간지지(天干地支) 연도 체계입니다.\n년륜(年輪)에는 **6개의 색상 표시(5개 행성과 달)가** 있습니다. 그 위치를 이해하려면, 24절기가 날짜이자 황도의 위치라는 사실부터 시작해야 합니다. 예를 들어, 춘분은 춘분일에 태양이 황도면상의 위치입니다. 예를 들어, 목성이 춘분에 있다는 것은 그날 태양이 있던 같은 방향에 있다는 것을 의미합니다. 수성과 금성의 위치는 항상 태양 근처에 있습니다. 왜냐하면 그들의 궤도는 지구 궤도 내부에 있기 때문입니다. 그러나 화성, 목성, 토성의 위치는 태양 근처에 있지 않을 수 있습니다. 태양 앞에 있는 행성들(년륜(年輪)의 투명 부분)은 일출 전에 뜨고 일몰 전에 지며, 태양 뒤에 있는 행성들(고체 색 부분)은 일출 후에 뜨고 일몰 후에 집니다.\n# 월륜(月輪)의 색상 표시는 무엇인가요?\n일반적으로 4가지 유형이 있습니다: **신월, 만월, 절기** 및 **중기(中氣)** 로, 정확한 색상은 설정에서 변경할 수 있습니다. 윤달이 \"순간의 정밀도\"으로 설정되면, 월륜(月輪)은 신월 순간부터 시작되므로, 신월 표시의 필요성이 없어져, 다른 세 가지 색깔 표시만 남습니다. 보름은 한 달 중 달이 가장 찬란한 순간을 표시하며, 몇 달 동안 관찰하여 보름이 15, 16, 또는 17일에 있는지 알 수 있습니다. 절기 표시도 년륜(年輪)의의 24절기와 일치합니다.\n신월, 만월 또는 절기 시간에 가까워지면, 같은 색깔의 표시도 일륜(日輪)과 시륜(時輪)의 **외곽**에 나타납니다. 이 네 가지 색깔 표시는 일륜(日輪)과 시륜(時輪)의 외곽에 나타납니다.\n# 일월출몰(日月出沒) 시간\n일출, 일몰, 월출 및 월몰 시간은 고대 사람들에게 중요했으며 농업에서 필수적이었습니다. 따라서 이는 화력(華曆)에 없어서는 안 될 부분입니다.\n설정에서 위치가 활성화되면, 일륜(日輪)의 **내부 가장자리**에 있는 지역의 일월출몰 시간이 표시됩니다. 이러한 시간에 가까워지면, 시륜(時輪)의 내부 가장자리에도 같은 색깔의 표시가 나타납니다. 이 범주에는 7가지 색깔 표시가 있습니다: **일출, 정오, 일몰, 자정, 월출, 월정(月正)** 및 **월몰**. 특정 색상도 설정에서 변경할 수 있습니다. 태양시 설정이 실제 태양시로 설정되면, 정오와 자정은 색깔 표시로 표시되지 않습니다. 왜냐하면 그 시간 자체로 이미 명확하기 때문입니다.\n# 용어 설명\n**절기**는 지구가 궤도상의 위치입니다. 4개의 절기는 유명합니다: 동지, 춘분, 하지, 추분. 각각 사이의 90° 영역은 6개의 더 작은 구분으로 나뉘며, 각 사분면에 5개의 추가 절기가 있습니다. 총 24개의 절기가 있으며, 황도면에서 15°씩 떨어져 있습니다. 절기의 홀수 번째도 마찬가지로 불립니다. 12개는 다음과 같습니다: 소한(小寒), 입춘(立春), 경칩(驚蟄), 청명(清明), 입하(立夏), 망종(芒種), 소서(小暑), 입추(立秋), 백로(白露), 한로(寒露), 입동(立冬) 및 대설(大雪). 경칩(驚蟄)와 청명(清明)은 원래 85AC 이전에는 짝수 절기였으나, 雨水와 穀雨와 교체되었습니다.\n**중기(中氣)** 는 절기 중 짝수이며, 서로 30°씩 떨어져 있습니다. 이들은 화력(華曆)에서 윤달을 결정하는 데 도움이 됩니다(자세한 내용은 \"화력(華曆)이란 무엇인가\" 섹션을 참조하세요). 12개는 다음과 같습니다: 동지, 대한(大寒), 우수(雨水), 춘분, 곡우(穀雨), 소만(小滿), 하지, 대서(大暑), 처서(處暑), 추분, 상강(霜降) 및 소설(小雪).\n**신월**은 태양과 달이 같은 황도 경도(태양일식 중에는 위도)에 위치할 때의 순간입니다.\n**만월**은 태양과 달이 지구를 사이에 두고 서로 반대편에 위치할 때의 순간입니다(이때 달의 일식이 일어날 수 있습니다).\n**시(時) 이름**은 12지지(地支)로, 오늘날 2시간마다 나뉘어져 있습니다. 12am부터 12pm 전까지는: 자(子), 축(丑), 인(寅), 묘(卯), 진(辰), 사(巳)이고, 12pm부터 다음 날 12am 전까지는: 오(午), 미(未), 신(申), 유(酉), 술(戌) 및 해(亥)입니다.\n**오성(五星)** 의 고명은 현재와 다릅니다. 진성(辰星)은 현재의 수성으로, '성'이라고도 불립니다; 태백(太白)은 현재의 금성으로, 하늘에서 가장 밝은 별이기 때문에 태백이라고 불립니다; 형혹(熒惑)은 현재의 화성입니다; 세성(歳星)은 현재의 목성으로, 태세(太歳)는 세성과 관련이 있지만 다른 개념입니다; 천성(填星) 또는 진성(鎮星)은 현재의 토성입니다.\n**월명**: 1월부터 10월까지는 순서대로 일월에서 십월까지 표기하고, 11월은 동월(冬月)로, 이는 동지가 이 달에 포함되기 때문입니다. 12월은 납월(臘月)로, 고대에 12월의 술일(戌日)에 납제(臘祭) 행사를 진행했기 때문입니다. 12개의 월명이 모두 두 글자가 되도록 유지하기 위해, 화력(華曆)은 동월(冬月), 납월(臘月)을 사용하고 11월, 12월은 사용하지 않습니다.\n\n" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "# 何以要做此华历?\n华历即**中华日历**。华历在生活中还有用吗?其实基本无用了,但作为一种文化传承,依然可以作为精美的装点。见惯了千篇一律,透露出十几年前那既不传统,又不现代的万年历,我就一直想做一款现代的华历,于是就做了。\n灵感取自表,把年月也仿似日、时一般做成轮。如此,年月日时皆一目了然,一年中的廿四节气、大小月、闰月亦能聚在一盘,直观呈现。\n# 华历是什么?\n华历是**阴阳历**,亦系天文历,一切皆以天文定,理念朴素,自有美感;但亦因此而难于计算,所幸在现代技术面前,计算不再是问题。以前每每感念新年年年不同,日历下方的小字上的华历日期也无甚规律,可当深入研究后才发现它的规则是如此简单,而所引出的计算又是无比复杂。\n阴阳历中的“阴”指的就系月了,新月,即月与日经度重叠的那一刻(如纬度亦重叠则为日蚀),所在之日为初一。因为日与月同经,同升共落,那一日看不见月,这是非常易于观测的天象,而月圆则没那么精确,圆一点、缺一点,幅度不大时并不明显,古人以新月定初一是很朴素的。所以第一条规则即系:**新月所在之日为初一,初一至下一初一前一日为一月**。\n阴阳历中的“阳”指的系日,月是以月定的,但没有定这个月是几月。给月定名是靠太阳完成的。为了让同样的月总是处在类似的季节,便有了第二条规定:**冬至必定在冬月(十一月)**。冬至是廿四节气中最重要的,其它节气对应的月可以有前后出入,之所以是冬至,因为冬至是北半球正午日影最长的一日,比其它除了夏至外的节气都更易观测,而选冬至不选夏至可能是因为冬季比较闲,无所事事的人就把冬至过成了一个重大节日,就显得比夏至重要了。\n冬月定了,到下个冬月之间的月就按顺序取名,如果中间正好有11个整月,那么完美。但一年365.24日中平均有12.37个平均为29.53日的月,有时候两个冬月之间有12个整G月,多了一个月,就得置闰月调整,这就麻烦了。想了一想,选了廿四节气中的十二个叫中气,两个冬至之间必定有且只有11个中气,如果有12个月,必定至少有一个月是无中气的,这倒霉月就叫闰月。如果碰巧有的月占了两个中气,就可能有两个无中气月,不能都闰了。总结就是:**两个冬至之间若有13个朔,则首个无中气月为闰月**。\n# 既然是天文历,会不会时区不同,计算出的日期也不同呢\n会的。\n华历的定义是从天象来的,古人都在东亚,走不出多远,天象都差不多;而现在视野开阔了,有了全球的概念,问题就复杂了。譬如长安23日早8点朔,23日即初一;而在纽约是22日晚7点朔,22日为初一。因此同样使用这套历法的中国、日本、韩国、越南历,因为时差的缘故,可能初一的日期就略有不同。\n初一有前后一日的出入,闰月的出入就更大了。中气的平均间隔是30.44日,月平均长29.53日,相差不大,无中气月的前后必定紧邻前后两个中气。初一稍有不同,则哪个月无中气就会有巨大差别,前后可以相差4个月。一日的出入可以接受,四个月的出入就难以接受了。\n所以这里提供了另一种置闰法:中气包含的计算规则,由初一至下个初一之间,改为朔(精确时刻)至下个朔之间。随时区不同,中气可能落在不同日期,但朔时刻与中气时刻之间的先后关系是不随时区而变化的。此即“**精确至时刻**”选项,默认不开启,需手动打开。\n# 时、刻又是什么\n午时三刻是众所难忘的台词,午时三刻究竟系几点?时与刻是何关系,想必是常见的疑问。其实时与刻是两种全然不同的计时方法。\n十二辰本是天上十二个星域,用以记年的,最古老的时辰并不固定是十二个,有十个者,也有十六个者,时长亦不定,而以自然现象或作息命名,如旦、昏、朝食、人定……。最早的精确时计是漏,漏上画好刻。**一天分为百刻,一刻合今14分24秒**。但百刻能把人眼看花,古人把十二辰用以计时,同时结合了百刻,出现了某时某刻的说法。即在百刻之上同时画上十二个时辰,在过了某个时辰后就只数该时辰后多少刻。如此大大减轻了眼睛的负担,再也不会数错了。\n但问题来了,时辰之间间隔120分钟,刻之间间隔14分24秒,不能整除。所以子、卯、午、酉四时辰与刻完美重合,其它时辰则不重合。而且时辰之后第一刻所代表的时长并不相同。子、卯、午、酉后第一刻是完整的一刻,其它时辰后第一刻则不完整。一小时60分钟与一刻14分24秒的最大公约数为2分24秒,是为一小刻,一刻内有6小刻,在最内轮中画出了小刻。\n有一点需要明确的是,古代的时辰是一个时刻,非时段,子时就是0:00那一时刻,而并非前一日23:00至后一日1:00那两小时。子时三刻指子时后又过了三刻,而不是子时中的第三刻。\n至于为何时辰会有一段时间,如子时为23:00-1:00的印象?简单来说就是随着时计进步,出现了一种显示时辰牌的时钟,12:00时“午时”出现在窗口正中,而时辰牌不可能突然凭空出现,所以从11:00开始,“午时”牌出现在窗口角落,12:00在正中,13:00离开视线,这一段时间被叫做午时。在这之后,一个时辰被分成两小时,前一小时为某时“初”,后一小时为某时“正”。\n# 真太阳时和标准时\n当今计时使用时区,譬如使用东八区时,正午就是东经120°处之正午,凡不在东经120°之处者,真正的正午时间并不是标准时的12时,此间有**经度时差**。此外,因地球绕日所行非圆,在近日点附近绕行更速,此时一日略长于平均;而于远日点附近绕行更徐,一日略饾于平均,这也会影响正午时刻,这个差值叫**真平时差**。\n标准时即日常所用之时,而真太阳时则系校正此二项差值后之时。真太阳时的午正即当日太阳行经最高点之时,子正即太阳处于地球背后正对面之时。标准时的午正、子正则并无特别天文含义。\n# 年轮上的色块是何物\n在华历中,除了计日、计时,**五行星位置(辰、太白、荧惑、填、岁)** 也是必备内容。当今有了现代天文学,行星和日月行迹都能精确计算了。其中填星和岁星位置曾在古代用于计年,如岁在大荒落、岁在辰,岁绕行太阳一周11.86年,约为12年,故岁星纪年演变为地支纪年,填星绕行一周29.5年,填与岁合并,约60年一周期,由此诞生演用至今的干支纪年。\n年轮上有**6个色块(5星+月)**。24节气既是日期,也是天球上的位置,如“清明”即清明时刻太阳所处的黄道位置,而岁星在清明即岁星在同一个黄道位置。辰星与太白星因处于地球轨道内,因此它们总是在太阳(即日轮进度条末位)附近,荧惑、填、岁则未必。而位于太阳前(即年轮上虚色部分)的星会在日出前升空,日入前入地;位于太阳后(即实色部份)的星会在日出后升空,日入后入地。\n# 月轮上的色块是何物\n一般有4种:**朔、望、节、气**,具体颜色可于设置内调整。若选精确至时刻置闰法,则月轮始自朔之刻,此时不标朔,只余另三种色块。望刻所在月最圆,可多观察几个月,看月圆究竟是十五还是十六,抑或是十七?气与闰月息息相关,无气之月一般是闰月。节气色块可与年轮上所标24节气相对应。\n当接近朔、望、节、气时刻时,同样的色块也会出现在日轮和时轮上,以便更精准定时。这四种色块出现于日、时轮时位于轮**外侧**。\n# 日月出入时刻\n日出入时刻对古人来说非常重要,与廿四节气同属对农事最重要的部份,系华历中不可或缺的。\n如果打开了定位,则可计算当地日、月出入时刻,显示于日轮**内侧**,当接近某一时刻时,相同色块亦会出现于时轮上,同样位于内侧。此类色块共有7种:**日出、日中、日入、夜半、月出、月中、月入**,具体颜色亦可经设置调整。若开启了真太阳时,因午正即日中、子正即半夜,此二类不再标出。\n# 名词简介\n**节气**其实并无特别名称,但为与中气相区别,此处指节气中的奇数。含:小寒、立春、惊蛰(启蛰)、清明、立夏、芒种、小暑、立秋、白露、寒露、立冬、大雪,共12个。惊蛰(启蛰)、清明二节在后汉之前为中气,后汉元和二年分别与雨水、谷雨对调。\n**中气**指节气中的偶数。含:冬至、大寒、雨水、春分、谷雨、小满、夏至、大暑、处暑、秋分、霜降、小雪,共12个。气是华历中重要的部份,无中气月与闰月有关(详见“华历是什么”条)。\n**朔**指月与日共处同一天球经度之时刻,因此月与日同出同入,月华为日光所蔽,故无月。日食发生于此刻。\n**望**指月运行至日之正对面(地球位于二者之间),日入时月出,日出时月入,故整夜可见月。月食发生于此刻。\n**五星**之古名与今不同。辰星即今之水星,又单名“星”;太白星即今之金星,以其为天空最亮之星,故名太白;荧惑即今之火星;岁星即今之木星,太岁乃与岁星相关但不同的概念;填星又作镇星,即今之土星。" + "value" : "# 何以要做此华历?\n华历即**中华日历**。华历在生活中还有用吗?其实基本无用了,但作为一种文化传承,依然可以作为精美的装点。见惯了千篇一律,透露出十几年前那既不传统,又不现代的万年历,我就一直想做一款现代的华历,于是就做了。\n灵感取自表,把年月也仿似日、时一般做成轮。如此,年月日时皆一目了然,一年中的廿四节气、大小月、闰月亦能聚在一盘,直观呈现。\n# 华历是什么?\n华历是**阴阳历**,亦系天文历,一切皆以天文定,理念朴素,自有美感;但亦因此而难于计算,所幸在现代技术面前,计算不再是问题。以前每每感念新年年年不同,日历下方的小字上的华历日期也无甚规律,可当深入研究后才发现它的规则是如此简单,而所引出的计算又是无比复杂。\n阴阳历中的“阴”指的就系月了,新月,即月与日经度重叠的那一刻(如纬度亦重叠则为日蚀),所在之日为初一。因为日与月同经,同升共落,那一日看不见月,这是非常易于观测的天象,而月圆则没那么精确,圆一点、缺一点,幅度不大时并不明显,古人以新月定初一是很朴素的。所以第一条规则即系:**新月所在之日为初一,初一至下一初一前一日为一月**。\n阴阳历中的“阳”指的系日,月是以月定的,但没有定这个月是几月。给月定名是靠太阳完成的。为了让同样的月总是处在类似的季节,便有了第二条规定:**冬至必定在冬月(十一月)**。冬至是廿四节气中最重要的,其它节气对应的月可以有前后出入,之所以是冬至,因为冬至是北半球正午日影最长的一日,比其它除了夏至外的节气都更易观测,而选冬至不选夏至可能是因为冬季比较闲,无所事事的人就把冬至过成了一个重大节日,就显得比夏至重要了。\n冬月定了,到下个冬月之间的月就按顺序取名,如果中间正好有11个整月,那么完美。但一年365.24日中平均有12.37个平均为29.53日的月,有时候两个冬月之间有12个整G月,多了一个月,就得置闰月调整,这就麻烦了。想了一想,选了廿四节气中的十二个叫中气,两个冬至之间必定有且只有11个中气,如果有12个月,必定至少有一个月是无中气的,这倒霉月就叫闰月。如果碰巧有的月占了两个中气,就可能有两个无中气月,不能都闰了。总结就是:**两个冬至之间若有13个朔,则首个无中气月为闰月**。\n# 既然是天文历,会不会时区不同,计算出的日期也不同呢\n会的。\n华历的定义是从天象来的,古人都在东亚,走不出多远,天象都差不多;而现在视野开阔了,有了全球的概念,问题就复杂了。譬如长安23日早8点朔,23日即初一;而在纽约是22日晚7点朔,22日为初一。因此同样使用这套历法的中国、日本、韩国、越南历,因为时差的缘故,可能初一的日期就略有不同。\n初一有前后一日的出入,闰月的出入就更大了。中气的平均间隔是30.44日,月平均长29.53日,相差不大,无中气月的前后必定紧邻前后两个中气。初一稍有不同,则哪个月无中气就会有巨大差别,前后可以相差4个月。一日的出入可以接受,四个月的出入就难以接受了。\n所以这里提供了另一种置闰法:中气包含的计算规则,由初一至下个初一之间,改为朔(精确时刻)至下个朔之间。随时区不同,中气可能落在不同日期,但朔时刻与中气时刻之间的先后关系是不随时区而变化的。此即“**精确至时刻**”选项,默认不开启,需手动打开。\n# 时、刻又是什么\n午时三刻是众所难忘的台词,午时三刻究竟系几点?时与刻是何关系,想必是常见的疑问。其实时与刻是两种全然不同的计时方法。\n十二辰本是天上十二个星域,用以记年的,最古老的时辰并不固定是十二个,有十个者,也有十六个者,时长亦不定,而以自然现象或作息命名,如旦、昏、朝食、人定……。最早的精确时计是漏,漏上画好刻。**一天分为百刻,一刻合今14分24秒**。但百刻能把人眼看花,古人把十二辰用以计时,同时结合了百刻,出现了某时某刻的说法。即在百刻之上同时画上十二个时辰,在过了某个时辰后就只数该时辰后多少刻。如此大大减轻了眼睛的负担,再也不会数错了。\n但问题来了,时辰之间间隔120分钟,刻之间间隔14分24秒,不能整除。所以子、卯、午、酉四时辰与刻完美重合,其它时辰则不重合。而且时辰之后第一刻所代表的时长并不相同。子、卯、午、酉后第一刻是完整的一刻,其它时辰后第一刻则不完整。一小时60分钟与一刻14分24秒的最大公约数为2分24秒,是为一小刻,一刻内有6小刻,在最内轮中画出了小刻。\n有一点需要明确的是,古代的时辰是一个时刻,非时段,子时就是0:00那一时刻,而并非前一日23:00至后一日1:00那两小时。子时三刻指子时后又过了三刻,而不是子时中的第三刻。每个时辰过后可以有0(初)至8刻。\n至于为何时辰会有一段时间,如子时为23:00-1:00的印象?简单来说就是随着时计进步,出现了一种显示时辰牌的时钟,12:00时“午时”出现在窗口正中,而时辰牌不可能突然凭空出现,所以从11:00开始,“午时”牌出现在窗口角落,12:00在正中,13:00离开视线,这一段时间被叫做午时。在这之后,一个时辰被分成两小时,前一小时为某时“初”,后一小时为某时“正”,在设置中可选是否分初正。\n# 真太阳时和标准时\n当今计时使用时区,譬如使用东八区时,正午就是东经120°处之正午,凡不在东经120°之处者,真正的正午时间并不是标准时的12时,此间有**经度时差**。此外,因地球绕日所行非圆,在近日点附近绕行更速,此时一日略长于平均;而于远日点附近绕行更徐,一日略饾于平均,这也会影响正午时刻,这个差值叫**真平时差**。\n标准时即日常所用之时,而真太阳时则系校正此二项差值后之时。真太阳时的午正即当日太阳行经最高点之时,子正即太阳处于地球背后正对面之时。标准时的午正、子正则并无特别天文含义。\n# 年轮上的色块是何物\n在华历中,除了计日、计时,**五行星位置(辰、太白、荧惑、填、岁)** 也是必备内容。当今有了现代天文学,行星和日月行迹都能精确计算了。其中填星和岁星位置曾在古代用于计年,如岁在大荒落、岁在辰,岁绕行太阳一周11.86年,约为12年,故岁星纪年演变为地支纪年,填星绕行一周29.5年,填与岁合并,约60年一周期,由此诞生演用至今的干支纪年。\n年轮上有**6个色块(5星+月)**。24节气既是日期,也是天球上的位置,如“清明”即清明时刻太阳所处的黄道位置,而岁星在清明即岁星在同一个黄道位置。辰星与太白星因处于地球轨道内,因此它们总是在太阳(即日轮进度条末位)附近,荧惑、填、岁则未必。而位于太阳前(即年轮上虚色部分)的星会在日出前升空,日入前入地;位于太阳后(即实色部份)的星会在日出后升空,日入后入地。\n# 月轮上的色块是何物\n一般有4种:**朔、望、节、气**,具体颜色可于设置内调整。若选精确至时刻置闰法,则月轮始自朔之刻,此时不标朔,只余另三种色块。望刻所在月最圆,可多观察几个月,看月圆究竟是十五还是十六,抑或是十七?气与闰月息息相关,无气之月一般是闰月。节气色块可与年轮上所标24节气相对应。\n当接近朔、望、节、气时刻时,同样的色块也会出现在日轮和时轮上,以便更精准定时。这四种色块出现于日、时轮时位于轮**外侧**。\n# 日月出入时刻\n日出入时刻对古人来说非常重要,与廿四节气同属对农事最重要的部份,系华历中不可或缺的。\n如果打开了定位,则可计算当地日、月出入时刻,显示于日轮**内侧**,当接近某一时刻时,相同色块亦会出现于时轮上,同样位于内侧。此类色块共有7种:**日出、日中、日入、夜半、月出、月中、月入**,具体颜色亦可经设置调整。若开启了真太阳时,因午正即日中、子正即半夜,此二类不再标出。\n# 名词简介\n**节气**其实并无特别名称,但为与中气相区别,此处指节气中的奇数。含:小寒、立春、惊蛰(启蛰)、清明、立夏、芒种、小暑、立秋、白露、寒露、立冬、大雪,共12个。惊蛰(启蛰)、清明二节在后汉之前为中气,后汉元和二年分别与雨水、谷雨对调。\n**中气**指节气中的偶数。含:冬至、大寒、雨水、春分、谷雨、小满、夏至、大暑、处暑、秋分、霜降、小雪,共12个。气是华历中重要的部份,无中气月与闰月有关(详见“华历是什么”条)。\n**朔**指月与日共处同一天球经度之时刻,因此月与日同出同入,月华为日光所蔽,故无月。日食发生于此刻。\n**望**指月运行至日之正对面(地球位于二者之间),日入时月出,日出时月入,故整夜可见月。月食发生于此刻。\n**五星**之古名与今不同。辰星即今之水星,又单名“星”;太白星即今之金星,以其为天空最亮之星,故名太白;荧惑即今之火星;岁星即今之木星,太岁乃与岁星相关但不同的概念;填星又作镇星,即今之土星。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "# 何以要做此華曆?\n華曆即**中華日曆**。華曆在生活中還有用嗎?其實基本無用了,但作爲一種文化傳承,依然可以作爲精美的裝點。見慣了千篇一律,透露出十幾年前那既不傳統,又不現代的萬年曆,我就一直想做一款現代的華曆,於是就做了。\n靈感取自錶,把年月也倣似日、時一般做成輪。如此,年月日時皆一目瞭然,一年中的廿四節氣、大小月、閏月亦能聚在一盤,直觀呈現。\n# 華曆是甚麼?\n華曆是**陰陽曆**,亦係天文曆,一切皆以天文定,理念樸素,自有美感;但亦因此而難於計算,所幸在現代技術面前,計算不再是問題。以前每每感念新年年年不同,日曆下方的小字上的華曆日期也無甚規律,可當深入研究後才發現它的規則是如此簡單,而所引出的計算又是無比複雜。\n陰陽曆中的「陰」指的就係月了,新月,即月與日經度重疊的那一刻(如緯度亦重疊則爲日蝕),所在之日爲初一。因爲日與月同經,同升共落,那一日看不見月,這是非常易於觀測的天象,而月圓則沒那麼精確,圓一點、缺一點,幅度不大時並不明顯,古人以新月定初一是很樸素的。所以第一條規則即係:**新月所在之日爲初一,初一至下一初一前一日爲一月**。\n陰陽曆中的「陽」指的係日,月是以月定的,但沒有定這個月是幾月。給月定名是靠太陽完成的。爲了讓同樣的月總是處在類似的季節,便有了第二條規定:**冬至必定在冬月(十一月)**。冬至是廿四節氣中最重要的,其它節氣對應的月可以有前後出入,之所以是冬至,因爲冬至是北半球正午日影最長的一日,比其它除了夏至外的節氣都更易觀測,而選冬至不選夏至可能是因爲冬季比較閒,無所事事的人就把冬至過成了一個重大節日,就顯得比夏至重要了。\n冬月定了,到下個冬月之間的月就按順序取名,如果中間正好有11個整月,那麼完美。但一年365.24日中平均有12.37個平均爲29.53日的月,有時候兩個冬月之間有12個整月,多了一個月,就得置閏月調整,這就麻煩了。想了一想,選了廿四節氣中的十二個叫中氣,兩個冬至之間必定有且只有11個中氣,如果有12個月,必定至少有一個月是無中氣的,這倒霉月就叫閏月。如果碰巧有的月佔了兩個中氣,就可能有兩個無中氣月,不能都閏了。總結就是:**兩個冬至之間若有13個朔,則首個無中氣月爲閏月**。\n# 既然是天文曆,會不會時區不同,計算出的日期也不同呢\n會的。\n華曆的定義是從天象來的,古人都在東亞,走不出多遠,天象都差不多;而現在視野開闊了,有了全球的概念,問題就複雜了。譬如長安23日早8點朔,23日即初一;而在紐約是22日晚7點朔,22日爲初一。因此同樣使用這套曆法的中國、日本、韓國、越南曆,因爲時差的緣故,可能初一的日期就略有不同。\n初一有前後一日的出入,閏月的出入就更大了。中氣的平均間隔是30.44日,月平均長29.53日,相差不大,無中氣月的前後必定緊鄰前後兩個中氣。初一稍有不同,則哪個月無中氣就會有巨大差別,前後可以相差4個月。一日的出入可以接受,四個月的出入就難以接受了。\n所以這裏提供了另一種置閏法:中氣包含的計算規則,由初一至下個初一之間,改爲朔(精確時刻)至下個朔之間。隨時區不同,中氣可能落在不同日期,但朔時刻與中氣時刻之間的先後關係是不隨時區而變化的。此即「**精確至時刻**」選項,默認不開啓,需手動打開。\n# 時、刻又是甚麼\n午時三刻是眾所難忘的台詞,午時三刻究竟係幾點?時與刻是何關係,想必是常見的疑問。其實時與刻是兩種全然不同的計時方法。\n十二辰本是天上十二個星域,用以記年的,最古老的時辰並不固定是十二個,有十個者,也有十六個者,時長亦不定,而以自然現象或作息命名,如旦、昏、朝食、人定……。最早的精確時計是漏,漏上畫好刻。**一天分爲百刻,一刻合今14分24秒**。但百刻能把人眼看花,古人把十二辰用以計時,同時結合了百刻,出現了某時某刻的說法。即在百刻之上同時畫上十二個時辰,在過了某個時辰後就只數該時辰後多少刻。如此大大減輕了眼睛的負擔,再也不會數錯了。\n但問題來了,時辰之間間隔120分鐘,刻之間間隔14分24秒,不能整除。所以子、卯、午、酉四時辰與刻完美重合,其它時辰則不重合。而且時辰之後第一刻所代表的時長並不相同。子、卯、午、酉後第一刻是完整的一刻,其它時辰後第一刻則不完整。一小時60分鐘與一刻14分24秒的最大公約數爲2分24秒,是爲一小刻,一刻內有6小刻,在最內輪中畫出了小刻。\n有一點需要明確的是,古代的時辰是一個時刻,非時段,子時就是0:00那一時刻,而並非前一日23:00至後一日1:00那兩小時。子時三刻指子時後又過了三刻,而不是子時中的第三刻。\n至於爲何時辰會有一段時間,如子時爲23:00-1:00的印象?簡單來說就是隨著時計進步,出現了一種顯示時辰牌的時鐘,12:00時「午時」出現在窗口正中,而時辰牌不可能突然憑空出現,所以從11:00開始,「午時」牌出現在窗口角落,12:00在正中,13:00離開視線,這一段時間被叫做午時。在這之後,一個時辰被分成兩小時,前一小時爲某時「初」,後一小時爲某時「正」。\n# 真太陽時和標準時\n當今計時使用時區,譬如使用東八區時,正午就是東經120°處之正午,凡不在東經120°之處者,真正的正午時間並不是標準時的12時,此間有**經度時差**。此外,因地球繞日所行非圓,在近日點附近繞行更速,此時一日略長於平均;而於遠日點附近繞行更徐,一日略餖於平均,這也會影響正午時刻,這個差值叫**真平時差**。\n標準時即日常所用之時,而真太陽時則係校正此二項差值後之時。真太陽時的午正即當日太陽行經最高點之時,子正即太陽處於地球背後正對面之時。標準時的午正、子正則並無特別天文含義。\n# 年輪上的色塊是何物\n在華曆中,除了計日、計時,**五行星位置(辰、太白、熒惑、填、歲)** 也是必備內容。當今有了現代天文學,行星和日月行跡都能精確計算了。其中填星和歲星位置曾在古代用於計年,如歲在大荒落、歲在辰,歲繞行太陽一週11.86年,約爲12年,故歲星紀年演變爲地支紀年,填星繞行一週29.5年,填與歲合併,約60年一週期,由此誕生演用至今的干支紀年。\n年輪上有**6個色塊(5星+月)**。24節氣既是日期,也是天球上的位置,如「清明」即清明時刻太陽所處的黃道位置,而歲星在清明即歲星在同一個黃道位置。辰星與太白星因處於地球軌道內,因此它們總是在太陽(即日輪進度條末位)附近,熒惑、填、歲則未必。而位於太陽前(即年輪上虚色部分)的星會在日出前升空,日入前入地;位於太陽後(即實色部份)的星會在日出後升空,日入後入地。\n# 月輪上的色塊是何物\n一般有4種:**朔、望、節、氣**,具體顏色可於設置內調整。若選精確至時刻置閏法,則月輪始自朔之刻,此時不標朔,只餘另三種色塊。望刻所在月最圓,可多觀察幾個月,看月圓究竟是十五還是十六,抑或是十七?氣與閏月息息相關,無氣之月一般是閏月。節氣色塊可與年輪上所標24節氣相對應。\n當接近朔、望、節、氣時刻時,同樣的色塊也會出現在日輪和時輪上,以便更精準定時。這四種色塊出現於日、時輪時位於輪**外側**。\n# 日月出入時刻\n日出入時刻對古人來說非常重要,與廿四節氣同屬對農事最重要的部份,係華曆中不可或缺的。\n如果打開了定位,則可計算當地日、月出入時刻,顯示於日輪**內側**,當接近某一時刻時,相同色塊亦會出現於時輪上,同樣位於內側。此類色塊共有7種:**日出、日中、日入、夜半、月出、月中、月入**,具體顏色亦可經設置調整。若開啓了真太陽時,因午正即日中、子正即半夜,此二類不再標出。\n# 名詞簡介\n**節氣**其實並無特別名稱,但爲與中氣相區別,此處指節氣中的奇数。含:大雪、小寒、立春、驚蟄(啓蟄)、清明、立夏、芒種、小暑、立秋、白露、寒露、立冬,共12個。驚蟄(啓蟄)、清明二節在後漢之前爲中氣,後漢元和二年分別與雨水、穀雨對調。\n**中氣**指節氣中的偶數。含:冬至、大寒、雨水、春分、穀雨、小滿、夏至、大暑、處暑、秋分、霜降、小雪,共12個。氣是華曆中重要的部份,無中氣月與閏月有關(詳見「華曆是甚麼」條)。\n**朔**指月與日共處同一天球經度之時刻,因此月與日同出同入,月華爲日光所蔽,故無月。日食發生於此刻。\n**望**指月運行至日之正對面(地球位於二者之間),日入時月出,日出時月入,故整夜可見月。月食發生於此刻。\n**五星**之古名與今不同。辰星即今之水星,又單名「星」;太白星即今之金星,以其爲天空最亮之星,故名太白;熒惑即今之火星;歲星即今之木星,太歲乃與歲星相關但不同的概念;填星又作鎮星,即今之土星。" + "value" : "# 何以要做此華曆?\n華曆即**中華日曆**。華曆在生活中還有用嗎?其實基本無用了,但作爲一種文化傳承,依然可以作爲精美的裝點。見慣了千篇一律,透露出十幾年前那既不傳統,又不現代的萬年曆,我就一直想做一款現代的華曆,於是就做了。\n靈感取自錶,把年月也倣似日、時一般做成輪。如此,年月日時皆一目瞭然,一年中的廿四節氣、大小月、閏月亦能聚在一盤,直觀呈現。\n# 華曆是甚麼?\n華曆是**陰陽曆**,亦係天文曆,一切皆以天文定,理念樸素,自有美感;但亦因此而難於計算,所幸在現代技術面前,計算不再是問題。以前每每感念新年年年不同,日曆下方的小字上的華曆日期也無甚規律,可當深入研究後才發現它的規則是如此簡單,而所引出的計算又是無比複雜。\n陰陽曆中的「陰」指的就係月了,新月,即月與日經度重疊的那一刻(如緯度亦重疊則爲日蝕),所在之日爲初一。因爲日與月同經,同升共落,那一日看不見月,這是非常易於觀測的天象,而月圓則沒那麼精確,圓一點、缺一點,幅度不大時並不明顯,古人以新月定初一是很樸素的。所以第一條規則即係:**新月所在之日爲初一,初一至下一初一前一日爲一月**。\n陰陽曆中的「陽」指的係日,月是以月定的,但沒有定這個月是幾月。給月定名是靠太陽完成的。爲了讓同樣的月總是處在類似的季節,便有了第二條規定:**冬至必定在冬月(十一月)**。冬至是廿四節氣中最重要的,其它節氣對應的月可以有前後出入,之所以是冬至,因爲冬至是北半球正午日影最長的一日,比其它除了夏至外的節氣都更易觀測,而選冬至不選夏至可能是因爲冬季比較閒,無所事事的人就把冬至過成了一個重大節日,就顯得比夏至重要了。\n冬月定了,到下個冬月之間的月就按順序取名,如果中間正好有11個整月,那麼完美。但一年365.24日中平均有12.37個平均爲29.53日的月,有時候兩個冬月之間有12個整月,多了一個月,就得置閏月調整,這就麻煩了。想了一想,選了廿四節氣中的十二個叫中氣,兩個冬至之間必定有且只有11個中氣,如果有12個月,必定至少有一個月是無中氣的,這倒霉月就叫閏月。如果碰巧有的月佔了兩個中氣,就可能有兩個無中氣月,不能都閏了。總結就是:**兩個冬至之間若有13個朔,則首個無中氣月爲閏月**。\n# 既然是天文曆,會不會時區不同,計算出的日期也不同呢\n會的。\n華曆的定義是從天象來的,古人都在東亞,走不出多遠,天象都差不多;而現在視野開闊了,有了全球的概念,問題就複雜了。譬如長安23日早8點朔,23日即初一;而在紐約是22日晚7點朔,22日爲初一。因此同樣使用這套曆法的中國、日本、韓國、越南曆,因爲時差的緣故,可能初一的日期就略有不同。\n初一有前後一日的出入,閏月的出入就更大了。中氣的平均間隔是30.44日,月平均長29.53日,相差不大,無中氣月的前後必定緊鄰前後兩個中氣。初一稍有不同,則哪個月無中氣就會有巨大差別,前後可以相差4個月。一日的出入可以接受,四個月的出入就難以接受了。\n所以這裏提供了另一種置閏法:中氣包含的計算規則,由初一至下個初一之間,改爲朔(精確時刻)至下個朔之間。隨時區不同,中氣可能落在不同日期,但朔時刻與中氣時刻之間的先後關係是不隨時區而變化的。此即「**精確至時刻**」選項,默認不開啓,需手動打開。\n# 時、刻又是甚麼\n午時三刻是眾所難忘的台詞,午時三刻究竟係幾點?時與刻是何關係,想必是常見的疑問。其實時與刻是兩種全然不同的計時方法。\n十二辰本是天上十二個星域,用以記年的,最古老的時辰並不固定是十二個,有十個者,也有十六個者,時長亦不定,而以自然現象或作息命名,如旦、昏、朝食、人定……。最早的精確時計是漏,漏上畫好刻。**一天分爲百刻,一刻合今14分24秒**。但百刻能把人眼看花,古人把十二辰用以計時,同時結合了百刻,出現了某時某刻的說法。即在百刻之上同時畫上十二個時辰,在過了某個時辰後就只數該時辰後多少刻。如此大大減輕了眼睛的負擔,再也不會數錯了。\n但問題來了,時辰之間間隔120分鐘,刻之間間隔14分24秒,不能整除。所以子、卯、午、酉四時辰與刻完美重合,其它時辰則不重合。而且時辰之後第一刻所代表的時長並不相同。子、卯、午、酉後第一刻是完整的一刻,其它時辰後第一刻則不完整。一小時60分鐘與一刻14分24秒的最大公約數爲2分24秒,是爲一小刻,一刻內有6小刻,在最內輪中畫出了小刻。\n有一點需要明確的是,古代的時辰是一個時刻,非時段,子時就是0:00那一時刻,而並非前一日23:00至後一日1:00那兩小時。子時三刻指子時後又過了三刻,而不是子時中的第三刻。每個時辰過後可以有0(初)至8刻。\n至於爲何時辰會有一段時間,如子時爲23:00-1:00的印象?簡單來說就是隨著時計進步,出現了一種顯示時辰牌的時鐘,12:00時「午時」出現在窗口正中,而時辰牌不可能突然憑空出現,所以從11:00開始,「午時」牌出現在窗口角落,12:00在正中,13:00離開視線,這一段時間被叫做午時。在這之後,一個時辰被分成兩小時,前一小時爲某時「初」,後一小時爲某時「正」,在設置中可選是否分初正。\n# 真太陽時和標準時\n當今計時使用時區,譬如使用東八區時,正午就是東經120°處之正午,凡不在東經120°之處者,真正的正午時間並不是標準時的12時,此間有**經度時差**。此外,因地球繞日所行非圓,在近日點附近繞行更速,此時一日略長於平均;而於遠日點附近繞行更徐,一日略餖於平均,這也會影響正午時刻,這個差值叫**真平時差**。\n標準時即日常所用之時,而真太陽時則係校正此二項差值後之時。真太陽時的午正即當日太陽行經最高點之時,子正即太陽處於地球背後正對面之時。標準時的午正、子正則並無特別天文含義。\n# 年輪上的色塊是何物\n在華曆中,除了計日、計時,**五行星位置(辰、太白、熒惑、填、歲)** 也是必備內容。當今有了現代天文學,行星和日月行跡都能精確計算了。其中填星和歲星位置曾在古代用於計年,如歲在大荒落、歲在辰,歲繞行太陽一週11.86年,約爲12年,故歲星紀年演變爲地支紀年,填星繞行一週29.5年,填與歲合併,約60年一週期,由此誕生演用至今的干支紀年。\n年輪上有**6個色塊(5星+月)**。24節氣既是日期,也是天球上的位置,如「清明」即清明時刻太陽所處的黃道位置,而歲星在清明即歲星在同一個黃道位置。辰星與太白星因處於地球軌道內,因此它們總是在太陽(即日輪進度條末位)附近,熒惑、填、歲則未必。而位於太陽前(即年輪上虚色部分)的星會在日出前升空,日入前入地;位於太陽後(即實色部份)的星會在日出後升空,日入後入地。\n# 月輪上的色塊是何物\n一般有4種:**朔、望、節、氣**,具體顏色可於設置內調整。若選精確至時刻置閏法,則月輪始自朔之刻,此時不標朔,只餘另三種色塊。望刻所在月最圓,可多觀察幾個月,看月圓究竟是十五還是十六,抑或是十七?氣與閏月息息相關,無氣之月一般是閏月。節氣色塊可與年輪上所標24節氣相對應。\n當接近朔、望、節、氣時刻時,同樣的色塊也會出現在日輪和時輪上,以便更精準定時。這四種色塊出現於日、時輪時位於輪**外側**。\n# 日月出入時刻\n日出入時刻對古人來說非常重要,與廿四節氣同屬對農事最重要的部份,係華曆中不可或缺的。\n如果打開了定位,則可計算當地日、月出入時刻,顯示於日輪**內側**,當接近某一時刻時,相同色塊亦會出現於時輪上,同樣位於內側。此類色塊共有7種:**日出、日中、日入、夜半、月出、月中、月入**,具體顏色亦可經設置調整。若開啓了真太陽時,因午正即日中、子正即半夜,此二類不再標出。\n# 名詞簡介\n**節氣**其實並無特別名稱,但爲與中氣相區別,此處指節氣中的奇数。含:大雪、小寒、立春、驚蟄(啓蟄)、清明、立夏、芒種、小暑、立秋、白露、寒露、立冬,共12個。驚蟄(啓蟄)、清明二節在後漢之前爲中氣,後漢元和二年分別與雨水、穀雨對調。\n**中氣**指節氣中的偶數。含:冬至、大寒、雨水、春分、穀雨、小滿、夏至、大暑、處暑、秋分、霜降、小雪,共12個。氣是華曆中重要的部份,無中氣月與閏月有關(詳見「華曆是甚麼」條)。\n**朔**指月與日共處同一天球經度之時刻,因此月與日同出同入,月華爲日光所蔽,故無月。日食發生於此刻。\n**望**指月運行至日之正對面(地球位於二者之間),日入時月出,日出時月入,故整夜可見月。月食發生於此刻。\n**五星**之古名與今不同。辰星即今之水星,又單名「星」;太白星即今之金星,以其爲天空最亮之星,故名太白;熒惑即今之火星;歲星即今之木星,太歲乃與歲星相關但不同的概念;填星又作鎮星,即今之土星。" } } } @@ -685,6 +714,35 @@ } } }, + "分初正" : { + "comment" : "Large hour setting: small", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "60 min" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "初正を区別する" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "초정을 구분한다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分初正" + } + } + } + }, "列於四隅之時計" : { "localizations" : { "en" : { @@ -773,7 +831,7 @@ } }, "刪不得" : { - "comment" : "Cannot switch theme", + "comment" : "Cannot delete theme", "localizations" : { "en" : { "stringUnit" : { @@ -1961,6 +2019,35 @@ } } }, + "將丢失當前編輯,但不影響存檔" : { + "comment" : "will lose edit, but saves will be intact", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Will lose edit, but saves will be intact" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の編集内容は失われますが、保存ファイルには影響しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 편집 내용은 사라지지만 저장 파일에는 영향을 미치지 않습니다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将丢失当前编辑,但不影响存档" + } + } + } + }, "將主題書於紙上" : { "comment" : "Save File message", "localizations" : { @@ -2379,7 +2466,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "연륜(年輪)" + "value" : "연륜" } }, "zh-Hans" : { @@ -2505,6 +2592,34 @@ } } }, + "復原" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "元に戻す" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복원" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复原" + } + } + } + }, "怪哉" : { "comment" : "Alert: Location service disabled", "extractionState" : "manual", @@ -2682,13 +2797,24 @@ } } }, - "數據" : { - "comment" : "Data Source", + "文字" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Data" + "value" : "Text" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "본문" } }, "ja" : { @@ -2706,7 +2832,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "数据" + "value" : "文字" } } } @@ -3100,7 +3226,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "일륜(日輪)" + "value" : "일륜" } }, "zh-Hans" : { @@ -3342,6 +3468,34 @@ } } }, + "時空" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Time & Loc" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時空" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시공" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时空" + } + } + } + }, "時計" : { "localizations" : { "en" : { @@ -3454,6 +3608,34 @@ } } }, + "時辰" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Length of Hour" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時辰" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시진" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时辰" + } + } + } + }, "暗" : { "comment" : "dark theme", "localizations" : { @@ -3732,7 +3914,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "월륜(月輪)" + "value" : "월륜" } }, "zh-Hans" : { @@ -4031,13 +4213,24 @@ } } }, - "樣式" : { - "comment" : "Styles", + "樸素寫就之華曆" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Styles" + "value" : "Chinese Time in plain text" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "平易な言葉で書かれた華暦" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일반 단어로 된 화력" } }, "ja" : { @@ -4055,7 +4248,63 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "样式" + "value" : "朴素写就之华历" + } + } + } + }, + "次月相" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Moon Phase" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次の月相" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 달상" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "次月相" + } + } + } + }, + "次節氣" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Solar Term" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次の節気" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 절기" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "次节气" } } } @@ -5289,6 +5538,34 @@ } } }, + "經理" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "관리" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "经理" + } + } + } + }, "經緯度" : { "comment" : "Geo Location section", "localizations" : { @@ -5466,7 +5743,19 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mark Color" + "value" : "Marks" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "記号の色" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "색상표" } }, "ja" : { @@ -5718,6 +6007,34 @@ } } }, + "裝飾" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Addon" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "装飾" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "장식" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "装饰" + } + } + } + }, "西" : { "comment" : "W in geo location", "localizations" : { @@ -5892,6 +6209,34 @@ } } }, + "設計" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Design" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設計" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설계" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设计" + } + } + } + }, "註釋" : { "comment" : "Documentation View", "localizations" : { @@ -5910,7 +6255,7 @@ "ko" : { "stringUnit" : { "state" : "translated", - "value" : "문서" + "value" : "주석" } }, "zh-Hans" : { @@ -6185,7 +6530,19 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ring Color" + "value" : "Gradient" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "輪の色" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "륜의 색상" } }, "ja" : { @@ -6724,4 +7081,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Shared/Setting/Datetime.swift b/Shared/Setting/Datetime.swift index b3cd36a..bba4d85 100644 --- a/Shared/Setting/Datetime.swift +++ b/Shared/Setting/Datetime.swift @@ -122,6 +122,15 @@ fileprivate struct TimeZoneSelection: Equatable { } } + var largeHour: Bool { + get { + watchLayout?.largeHour ?? false + } set { + watchLayout?.largeHour = newValue + updateTime() + } + } + func setup(watchSetting: WatchSetting, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) { self.watchLayout = watchLayout self.watchSetting = watchSetting @@ -190,6 +199,18 @@ struct Datetime: View { } } } + + HStack { + Picker("時辰", selection: $dateManager.largeHour) { + ForEach([true, false], id: \.self) { largeHour in + if largeHour { + Text("不分初正", comment: "Large hour setting: large") + } else { + Text("分初正", comment: "Large hour setting: small") + } + } + } + } } .pickerStyle(.menu) diff --git a/Shared/Setting/Decoration.swift b/Shared/Setting/Decoration.swift new file mode 100644 index 0000000..4598d59 --- /dev/null +++ b/Shared/Setting/Decoration.swift @@ -0,0 +1,159 @@ +// +// Decoration.swift +// Chinendar +// +// Created by Leo Liu on 1/16/24. +// + +import SwiftUI + +struct SliderView: View { + @Binding var value: CGFloat + @State var currentValue: CGFloat = 0 + let min: CGFloat + let max: CGFloat + let label: Text + + var body: some View { +#if os(iOS) || os(visionOS) + VStack { + HStack { + label + Spacer() + + let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter + }() + Text(formatter.string(from: NSNumber(value: currentValue)) ?? "") + .frame(maxWidth: 40, alignment: .trailing) + } + Slider(value: $currentValue, in: min...max) { editing in + if !editing { + value = currentValue + } + } + .labelsHidden() + } + .onAppear { + currentValue = value + } +#elseif os(macOS) + HStack { + label + .frame(maxWidth: 150, alignment: .leading) + Slider(value: $currentValue, in: min...max) { editing in + if !editing { + value = currentValue + } + } + .labelsHidden() + + let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter + }() + Text(formatter.string(from: NSNumber(value: currentValue)) ?? "") + .frame(maxWidth: 40, alignment: .trailing) + } + .onAppear { + currentValue = value + } +#endif + } +} + +struct ThemedColorSettingCell: View { + let text: Text + @Binding var color: CGColor + @Binding var darkColor: CGColor + + var body: some View { + HStack { + text + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 10) + Text("明", comment: "Light theme") + .lineLimit(1) + .frame(alignment: .trailing) + .padding(.horizontal, 5) + ColorPicker("", selection: $color) + .labelsHidden() + .padding(.trailing, 10) + Divider() + Text("暗", comment: "dark theme") + .lineLimit(1) + .frame(alignment: .trailing) + .padding(.horizontal, 5) + ColorPicker("", selection: $darkColor) + .labelsHidden() + + } + } +} + +@MainActor +struct DecorationSetting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + + var body: some View { + Form { + Section(header: Text("透明度", comment: "Transparency sliders")) { + SliderView(value: watchLayout.binding(\.shadeAlpha), min: 0, max: 1, label: Text("殘圈透明", comment: "Inactive ring opacity")) + SliderView(value: watchLayout.binding(\.majorTickAlpha), min: 0, max: 1, label: Text("大刻透明", comment: "Major Tick opacity")) + SliderView(value: watchLayout.binding(\.minorTickAlpha), min: 0, max: 1, label: Text("小刻透明", comment: "Minor Tick opacity")) + } + Section(header: Text("明暗主題色", comment: "Watch face colors in light and dark themes")) { +#if os(visionOS) + HStack(spacing: 20) { + ColorSettingCell(text: Text("大刻色", comment: "Major tick color"), color: watchLayout.binding(\.majorTickColorDark)) + Divider() + ColorSettingCell(text: Text("小刻色", comment: "Major tick color"), color: watchLayout.binding(\.minorTickColorDark)) + } + HStack(spacing: 20) { + ColorSettingCell(text: Text("節氣刻", comment: "Major tick color"), color: watchLayout.binding(\.oddSolarTermTickColorDark)) + Divider() + ColorSettingCell(text: Text("中氣刻", comment: "Major tick color"), color: watchLayout.binding(\.evenSolarTermTickColorDark)) + } + HStack(spacing: 20) { + ColorSettingCell(text: Text("小字", comment: "Major tick color"), color: watchLayout.binding(\.fontColorDark)) + Divider() + ColorSettingCell(text: Text("核", comment: "Major tick color"), color: watchLayout.binding(\.innerColorDark)) + } + HStack(spacing: 20) { + ColorSettingCell(text: Text("底輪", comment: "Ring background"), color: watchLayout.binding(\.backColorDark)) + } +#else + ThemedColorSettingCell(text: Text("大刻色", comment: "Major tick color"), color: watchLayout.binding(\.majorTickColor), darkColor: watchLayout.binding(\.majorTickColorDark)) + ThemedColorSettingCell(text: Text("小刻色", comment: "Major tick color"), color: watchLayout.binding(\.minorTickColor), darkColor: watchLayout.binding(\.minorTickColorDark)) + ThemedColorSettingCell(text: Text("節氣刻", comment: "Major tick color"), color: watchLayout.binding(\.oddSolarTermTickColor), darkColor: watchLayout.binding(\.oddSolarTermTickColorDark)) + ThemedColorSettingCell(text: Text("中氣刻", comment: "Major tick color"), color: watchLayout.binding(\.evenSolarTermTickColor), darkColor: watchLayout.binding(\.evenSolarTermTickColorDark)) + ThemedColorSettingCell(text: Text("小字", comment: "Major tick color"), color: watchLayout.binding(\.fontColor), darkColor: watchLayout.binding(\.fontColorDark)) + ThemedColorSettingCell(text: Text("核", comment: "Major tick color"), color: watchLayout.binding(\.innerColor), darkColor: watchLayout.binding(\.innerColorDark)) + ThemedColorSettingCell(text: Text("底輪", comment: "Ring background"), color: watchLayout.binding(\.backColor), darkColor: watchLayout.binding(\.backColorDark)) +#endif + } + } + .formStyle(.grouped) + .navigationTitle(Text("輪色", comment: "Rings Color Setting")) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#endif + } +} + +#Preview("Decoration Setting") { + DecorationSetting() +} diff --git a/Shared/Setting/RingSetting.swift b/Shared/Setting/RingSetting.swift index 749e4c4..9115f4f 100644 --- a/Shared/Setting/RingSetting.swift +++ b/Shared/Setting/RingSetting.swift @@ -350,95 +350,7 @@ struct GradientSliderView: View { } } -struct SliderView: View { - @Binding var value: CGFloat - @State var currentValue: CGFloat = 0 - let min: CGFloat - let max: CGFloat - let label: Text - - var body: some View { -#if os(iOS) || os(visionOS) - VStack { - HStack { - label - Spacer() - - let formatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = 2 - formatter.minimumFractionDigits = 0 - return formatter - }() - Text(formatter.string(from: NSNumber(value: currentValue)) ?? "") - .frame(maxWidth: 40, alignment: .trailing) - } - Slider(value: $currentValue, in: min...max) { editing in - if !editing { - value = currentValue - } - } - .labelsHidden() - } - .onAppear { - currentValue = value - } -#elseif os(macOS) - HStack { - label - .frame(maxWidth: 150, alignment: .leading) - Slider(value: $currentValue, in: min...max) { editing in - if !editing { - value = currentValue - } - } - .labelsHidden() - - let formatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = 2 - formatter.minimumFractionDigits = 0 - return formatter - }() - Text(formatter.string(from: NSNumber(value: currentValue)) ?? "") - .frame(maxWidth: 40, alignment: .trailing) - } - .onAppear { - currentValue = value - } -#endif - } -} - -struct ThemedColorSettingCell: View { - let text: Text - @Binding var color: CGColor - @Binding var darkColor: CGColor - - var body: some View { - HStack { - text - .lineLimit(1) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.trailing, 10) - Text("明", comment: "Light theme") - .lineLimit(1) - .frame(alignment: .trailing) - .padding(.horizontal, 5) - ColorPicker("", selection: $color) - .labelsHidden() - .padding(.trailing, 10) - Divider() - Text("暗", comment: "dark theme") - .lineLimit(1) - .frame(alignment: .trailing) - .padding(.horizontal, 5) - ColorPicker("", selection: $darkColor) - .labelsHidden() - } - } -} @MainActor struct RingSetting: View { @@ -467,41 +379,6 @@ struct RingSetting: View { GradientSliderView(text: Text("大字", comment: "Day Ring Gradient"), gradient: watchLayout.binding(\.centerFontColor), allowLoop: false) .frame(height: height - loopSize) } - Section(header: Text("透明度", comment: "Transparency sliders")) { - SliderView(value: watchLayout.binding(\.shadeAlpha), min: 0, max: 1, label: Text("殘圈透明", comment: "Inactive ring opacity")) - SliderView(value: watchLayout.binding(\.majorTickAlpha), min: 0, max: 1, label: Text("大刻透明", comment: "Major Tick opacity")) - SliderView(value: watchLayout.binding(\.minorTickAlpha), min: 0, max: 1, label: Text("小刻透明", comment: "Minor Tick opacity")) - } - Section(header: Text("明暗主題色", comment: "Watch face colors in light and dark themes")) { -#if os(visionOS) - HStack(spacing: 20) { - ColorSettingCell(text: Text("大刻色", comment: "Major tick color"), color: watchLayout.binding(\.majorTickColorDark)) - Divider() - ColorSettingCell(text: Text("小刻色", comment: "Major tick color"), color: watchLayout.binding(\.minorTickColorDark)) - } - HStack(spacing: 20) { - ColorSettingCell(text: Text("節氣刻", comment: "Major tick color"), color: watchLayout.binding(\.oddSolarTermTickColorDark)) - Divider() - ColorSettingCell(text: Text("中氣刻", comment: "Major tick color"), color: watchLayout.binding(\.evenSolarTermTickColorDark)) - } - HStack(spacing: 20) { - ColorSettingCell(text: Text("小字", comment: "Major tick color"), color: watchLayout.binding(\.fontColorDark)) - Divider() - ColorSettingCell(text: Text("核", comment: "Major tick color"), color: watchLayout.binding(\.innerColorDark)) - } - HStack(spacing: 20) { - ColorSettingCell(text: Text("底輪", comment: "Ring background"), color: watchLayout.binding(\.backColorDark)) - } -#else - ThemedColorSettingCell(text: Text("大刻色", comment: "Major tick color"), color: watchLayout.binding(\.majorTickColor), darkColor: watchLayout.binding(\.majorTickColorDark)) - ThemedColorSettingCell(text: Text("小刻色", comment: "Major tick color"), color: watchLayout.binding(\.minorTickColor), darkColor: watchLayout.binding(\.minorTickColorDark)) - ThemedColorSettingCell(text: Text("節氣刻", comment: "Major tick color"), color: watchLayout.binding(\.oddSolarTermTickColor), darkColor: watchLayout.binding(\.oddSolarTermTickColorDark)) - ThemedColorSettingCell(text: Text("中氣刻", comment: "Major tick color"), color: watchLayout.binding(\.evenSolarTermTickColor), darkColor: watchLayout.binding(\.evenSolarTermTickColorDark)) - ThemedColorSettingCell(text: Text("小字", comment: "Major tick color"), color: watchLayout.binding(\.fontColor), darkColor: watchLayout.binding(\.fontColorDark)) - ThemedColorSettingCell(text: Text("核", comment: "Major tick color"), color: watchLayout.binding(\.innerColor), darkColor: watchLayout.binding(\.innerColorDark)) - ThemedColorSettingCell(text: Text("底輪", comment: "Ring background"), color: watchLayout.binding(\.backColor), darkColor: watchLayout.binding(\.backColorDark)) -#endif - } } .formStyle(.grouped) .navigationTitle(Text("輪色", comment: "Rings Color Setting")) diff --git a/Shared/Setting/ThemesList.swift b/Shared/Setting/ThemesList.swift index 9d83a14..c81ca7e 100644 --- a/Shared/Setting/ThemesList.swift +++ b/Shared/Setting/ThemesList.swift @@ -7,6 +7,32 @@ import SwiftUI import SwiftData +import UniformTypeIdentifiers + +struct TextDocument: FileDocument { + init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + text = String(decoding: data, as: UTF8.self) + } + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + let data = Data(text.utf8) + return FileWrapper(regularFileWithContents: data) + } + + static var readableContentTypes: [UTType] = [.text] + + var text: String = "" + init?(_ text: String?) { + if let text = text { + self.text = text + } else { + return nil + } + } + +} private func loadThemes(data: [ThemeData]) -> [String: [ThemeData]] { var newThemes = [String: [ThemeData]]() @@ -35,7 +61,14 @@ struct ThemesList: View { @State private var createAlert = false @State private var switchAlert = false @State private var deleteAlert = false + @State private var revertAlert = false + @State private var errorAlert = false +#if os(iOS) || os(visionOS) + @State private var isExporting = false + @State private var isImporting = false +#endif @State private var newName = "" + @State private var errorMsg = "" @State private var target: ThemeData? = nil private var invalidName: Bool { let diviceName = target?.deviceName ?? currentDeviceName @@ -51,9 +84,12 @@ struct ThemesList: View { newName = validName(ThemeData.defaultName) createAlert = true } label: { - Label(NSLocalizedString("謄錄", comment: "Save current layout button"), systemImage: "plus") - .frame(maxWidth: .infinity, alignment: .center) - .foregroundStyle(Color.green) + Label("謄錄", systemImage: "square.and.pencil") + } + let revertBack = Button(role: .destructive) { + revertAlert = true + } label: { + Label("復原", systemImage: "arrow.clockwise") } let newThemeConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { @@ -62,7 +98,8 @@ struct ThemesList: View { do { try modelContext.save() } catch { - print("Save failed. \(error.localizedDescription)") + errorMsg = error.localizedDescription + errorAlert = true } } @@ -72,125 +109,133 @@ struct ThemesList: View { do { try modelContext.save() } catch { - print("Save failed. \(error.localizedDescription)") + errorMsg = error.localizedDescription + errorAlert = true } self.target = nil } } -#if os(macOS) + let readButton = Button { +#if os(macOS) readFile(context: modelContext) do { try modelContext.save() } catch { - print("Save failed. \(error.localizedDescription)") + errorMsg = error.localizedDescription + errorAlert = true } +#elseif os(iOS) || os(visionOS) + isImporting = true +#endif } label: { - Label(NSLocalizedString("讀入", comment: "Load from file button"), systemImage: "square.and.arrow.down") + Label("讀入", systemImage: "square.and.arrow.down") } -#endif - Form { -#if os(iOS) - Section { + let moreMenu = Menu { + VStack { newTheme + readButton + revertBack } -#endif + .labelStyle(.titleAndIcon) + } label: { + Label("經理", systemImage: "ellipsis.circle") + } + .menuIndicator(.hidden) + .menuStyle(.automatic) + + Form { let deviceNames = themes.keys.sorted(by: {$0 > $1}).sorted(by: {prev, _ in prev == currentDeviceName}) ForEach(deviceNames, id: \.self) { key in Section(key) { +#if os(iOS) + if key == currentDeviceName { + moreMenu + .labelStyle(.titleOnly) + .frame(maxWidth: .infinity) + } +#endif ForEach(themes[key]!, id: \.self) { theme in if !theme.isNil { - let deleteButton = Button { + let deleteButton = Button(role: .destructive) { target = theme deleteAlert = true } label: { - Label(NSLocalizedString("刪", comment: "Delete action"), systemImage: "trash") + Label("刪", systemImage: "trash") } - .tint(Color.red) let renameButton = Button { target = theme newName = validName(theme.name!) renameAlert = true } label: { - Label(NSLocalizedString("更名", comment: "Rename action"), systemImage: "rectangle.and.pencil.and.ellipsis.rtl") + Label("更名", systemImage: "rectangle.and.pencil.and.ellipsis.rtl") + } + + let applyButton = Button { + target = theme + switchAlert = true + } label: { + Label("用", systemImage: "cursorarrow.click.2") } - .tint(Color.indigo) -#if os(macOS) let saveButton = Button { +#if os(macOS) writeFile(theme: theme) +#elseif os(iOS) || os(visionOS) + target = theme + isExporting = true +#endif } label: { - Label(NSLocalizedString("寫下", comment: "Save to file button"), systemImage: "square.and.arrow.up") + Label("寫下", systemImage: "square.and.arrow.up") + } + + let dateLabel = if Calendar.current.isDate(theme.modifiedDate!, inSameDayAs: .now) { + Text(theme.modifiedDate!, style: .time) + .foregroundStyle(.secondary) + } else { + Text(theme.modifiedDate!, style: .date) + .foregroundStyle(.secondary) } -#endif - HStack { - Button { - target = theme - switchAlert = true - } label: { - Text(theme.name!) - } -#if os(iOS) || os(visionOS) - .buttonStyle(.borderless) -#elseif os(macOS) - .buttonStyle(.bordered) -#endif - .foregroundStyle(Color.primary) - -#if os(iOS) - .swipeActions(edge: .trailing) { - deleteButton - } - .swipeActions(edge: .leading) { - renameButton - } -#elseif os(macOS) || os(visionOS) - .contextMenu { - Button { - target = theme - switchAlert = true - } label: { - Label(NSLocalizedString("用", comment: "Switch to this"), systemImage: "cursorarrow.click.2") - } - .labelStyle(.titleAndIcon) - renameButton - .labelStyle(.titleAndIcon) - deleteButton - .labelStyle(.titleAndIcon) #if os(macOS) - saveButton - .labelStyle(.titleAndIcon) -#endif - } -#endif - Spacer() - if Calendar.current.isDate(theme.modifiedDate!, inSameDayAs: .now) { - Text(theme.modifiedDate!, style: .time) - .foregroundStyle(.secondary) - } else { - Text(theme.modifiedDate!, style: .date) - .foregroundStyle(.secondary) - } -#if os(macOS) || os(visionOS) + HStack { Menu { + applyButton renameButton - deleteButton -#if os(macOS) saveButton -#endif + deleteButton } label: { - Image(systemName: "ellipsis") + Text(theme.name!) } .menuIndicator(.hidden) .menuStyle(.button) - .buttonStyle(.borderless) + .buttonStyle(.accessoryBar) .labelStyle(.titleAndIcon) -#endif + Spacer() + dateLabel + } +#else + Menu { + applyButton + renameButton + saveButton + deleteButton + } label: { + HStack { + Text(theme.name!) + Spacer() + dateLabel + } } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.borderless) + .labelStyle(.titleAndIcon) + .tint(.primary) +#endif } } } @@ -236,7 +281,7 @@ struct ThemesList: View { } } } - .alert((target != nil && !target!.isNil) ? (NSLocalizedString("刪:", comment: "Confirm to delete theme message") + target!.name!) : NSLocalizedString("刪不得", comment: "Cannot switch theme"), isPresented: $deleteAlert) { + .alert((target != nil && !target!.isNil) ? (NSLocalizedString("刪:", comment: "Confirm to delete theme message") + target!.name!) : NSLocalizedString("刪不得", comment: "Cannot delete theme"), isPresented: $deleteAlert) { Button(NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), role: .cancel) { target = nil } Button(NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings"), role: .destructive) { if let target = target { @@ -244,30 +289,68 @@ struct ThemesList: View { do { try modelContext.save() } catch { - print("Save failed. \(error.localizedDescription)") + errorMsg = error.localizedDescription + errorAlert = true } } } } - .navigationTitle(Text("主題庫", comment: "manage saved themes")) -#if os(macOS) - .toolbar { - Menu { - newTheme - readButton - } label: { - Image(systemName: "ellipsis") + .alert("復原", isPresented: $revertAlert) { + Button(NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), role: .cancel) { } + Button(NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings"), role: .destructive) { + watchLayout.loadStatic() + } + } message: { + Text("將丢失當前編輯,但不影響存檔", comment: "will lose edit, but saves will be intact") + } + .alert("怪哉", isPresented: $errorAlert) { + Button("罷", role: .cancel) {} + } message: { + Text(errorMsg) + } +#if os(iOS) || os(visionOS) + .fileExporter(isPresented: $isExporting, + document: TextDocument(target?.code), + contentType: .text, + defaultFilename: target?.name.map {"\($0).txt"}) { result in + target = nil + if case .failure(let error) = result { + errorMsg = error.localizedDescription + errorAlert = true + } + } + .fileImporter(isPresented: $isImporting, allowedContentTypes: [.text]) { result in + switch result { + case .success(let file): + do { + let accessing = file.startAccessingSecurityScopedResource() + defer { + if accessing { + file.stopAccessingSecurityScopedResource() + } + } + let themeCode = try String(contentsOf: file) + let name = file.lastPathComponent + let namePattern = /^([^\.]+)\.?.*$/ + let themeName = try namePattern.firstMatch(in: name)?.output.1 + let theme = ThemeData(name: validName(themeName != nil ? String(themeName!) : name), code: themeCode) + modelContext.insert(theme) + try modelContext.save() + } catch { + errorMsg = error.localizedDescription + errorAlert = true + } + case .failure(let error): + errorMsg = error.localizedDescription + errorAlert = true } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.borderless) - .labelStyle(.titleAndIcon) } -#elseif os(visionOS) +#endif + .navigationTitle(Text("主題庫", comment: "manage saved themes")) +#if os(macOS) || os(visionOS) .toolbar { - newTheme + moreMenu } - #elseif os(iOS) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -289,6 +372,13 @@ struct ThemesList: View { } func validName(_ name: String, device: String? = nil) -> String { + func numberedName(_ baseName: String, number: Int) -> String { + if number <= 1 { + return baseName + } else { + return "\(baseName) \(i)" + } + } let namePattern = /^(.*) (\d+)$/ let baseName: String var i: Int @@ -297,12 +387,12 @@ struct ThemesList: View { i = Int(match.output.2)! } else { baseName = name - i = 2 + i = 1 } - while !validateName("\(baseName) \(i)", onDevice: device ?? currentDeviceName) { + while !validateName(numberedName(baseName, number: i), onDevice: device ?? currentDeviceName) { i += 1 } - return "\(baseName) \(i)" + return numberedName(baseName, number: i) } #if os(macOS) diff --git a/Shared/SourceHanSansKR-Heavy.otf b/Shared/SourceHanSansKR-Heavy.otf index 56196bc..42bca88 100644 Binary files a/Shared/SourceHanSansKR-Heavy.otf and b/Shared/SourceHanSansKR-Heavy.otf differ diff --git a/Shared/Views/RoundedRect.swift b/Shared/Views/RoundedRect.swift index a7c8365..4a31240 100644 --- a/Shared/Views/RoundedRect.swift +++ b/Shared/Views/RoundedRect.swift @@ -264,16 +264,17 @@ final class RoundedRect { } } -func anglePath(angle: CGFloat, startingAngle: CGFloat, in circle: RoundedRect) -> CGMutablePath { +func anglePath(angle: CGFloat, startingAngle: CGFloat, in circle: RoundedRect) -> (CGMutablePath, CGFloat, CGFloat, CGFloat) { let radius = sqrt(pow(circle._boundBox.width, 2)+pow(circle._boundBox.height, 2)) let center = CGPoint(x: circle._boundBox.midX, y: circle._boundBox.midY) let anglePoints = circle.arcPoints(lambdas: [startingAngle %% 1.0, (startingAngle+(startingAngle >= 0 ? angle : -angle)) %% 1.0]) let realStartingAngle = atan2(anglePoints[0].position.y - center.y, anglePoints[0].position.x - center.x) let realAngle = atan2(anglePoints[1].position.y - center.y, anglePoints[1].position.x - center.x) + let realLength = sqrt(pow(anglePoints[1].position.y - center.y, 2) + pow(anglePoints[1].position.x - center.x, 2)) let path = CGMutablePath() path.move(to: center) path.addLine(to: center+CGPoint(x: radius * cos(realStartingAngle), y: radius * sin(realStartingAngle))) path.addArc(center: center, radius: radius, startAngle: realStartingAngle, endAngle: realAngle, clockwise: startingAngle >= 0) path.closeSubpath() - return path + return (path, realStartingAngle, realAngle, realLength) } diff --git a/Shared/Views/WatchFaceBasics.swift b/Shared/Views/WatchFaceBasics.swift index 8a44f01..39e0d7b 100644 --- a/Shared/Views/WatchFaceBasics.swift +++ b/Shared/Views/WatchFaceBasics.swift @@ -48,6 +48,12 @@ struct WatchFont { #endif } +enum HighlightType { + case none + case alwaysOn + case flicker +} + struct ZeroRing: View { static let width: CGFloat = 0.05 let shortEdge: CGFloat @@ -114,14 +120,17 @@ struct ZeroRing: View { } struct Ring: View { - static let width: CGFloat = 0.075 + static let width: CGFloat = 0.07546 static let paddedWidth: CGFloat = 0.07546 let alpha: CGFloat + let width: CGFloat let majorTickAlpha: CGFloat let minorTickAlpha: CGFloat let shadowDirection: CGFloat let shadowSize: CGFloat let shortEdge: CGFloat + let startingAngle: Angle + let highlightAngle: Angle let outerRing: RoundedRect let outerRingPath: CGPath let majorTicksPath: CGPath @@ -133,13 +142,17 @@ struct Ring: View { let minorTickColor: CGColor let backColor: CGColor let gradient: Gradient + let highlightGradient: Gradient + let highlightType: HighlightType fileprivate let drawableTexts: [DrawableText] fileprivate let drawableMarks: [DrawableMark] - init(width: CGFloat, viewSize: CGSize, compact: Bool, ticks: ChineseCalendar.Ticks, startingAngle: CGFloat, angle: CGFloat, textFont: WatchFont, textColor: CGColor, alpha: CGFloat, majorTickAlpha: CGFloat, minorTickAlpha: CGFloat, majorTickColor: CGColor, minorTickColor: CGColor, backColor: CGColor, gradientColor: WatchLayout.Gradient, outerRing: RoundedRect, marks: [Marks], shadowDirection: CGFloat, entityNotes: EntityNotes?, shadowSize: CGFloat, offset: CGSize = .zero) { - self.shortEdge = min(viewSize.width, viewSize.height) + init(width: CGFloat, viewSize: CGSize, compact: Bool, ticks: ChineseCalendar.Ticks, startingAngle: CGFloat, angle: CGFloat, textFont: WatchFont, textColor: CGColor, alpha: CGFloat, majorTickAlpha: CGFloat, minorTickAlpha: CGFloat, majorTickColor: CGColor, minorTickColor: CGColor, backColor: CGColor, gradientColor: WatchLayout.Gradient, outerRing: RoundedRect, marks: [Marks], shadowDirection: CGFloat, entityNotes: EntityNotes?, shadowSize: CGFloat, highlightType: HighlightType, offset: CGSize = .zero) { + let shortEdge = min(viewSize.width, viewSize.height) + self.shortEdge = shortEdge let longEdge = max(viewSize.width, viewSize.height) self.alpha = alpha + self.width = width self.majorTickAlpha = majorTickAlpha self.minorTickAlpha = minorTickAlpha self.majorTickColor = majorTickColor @@ -179,7 +192,14 @@ struct Ring: View { } self.drawableTexts = drawableTexts self.textMaskPath = textMaskPath - self.pathWithAngle = anglePath(angle: angle, startingAngle: startingAngle, in: outerRing) + let (anglePath, realStartingAngle, realAngle, realLength) = anglePath(angle: angle, startingAngle: startingAngle, in: outerRing) + self.pathWithAngle = anglePath + self.startingAngle = Angle(radians: realStartingAngle) + self.highlightAngle = Angle(radians: realAngle) + self.highlightGradient = Gradient(stops: [.init(color: Color(white: 1, opacity: 0.5), location: 0), + .init(color: .clear, location: min(angle, width / 4 * shortEdge / realLength)), + .init(color: .clear, location: 1)]) + self.highlightType = highlightType var drawableMarks = [DrawableMark]() for mark in marks { @@ -209,64 +229,73 @@ struct Ring: View { var body: some View { let minorLineWidth = shortEdge/500 let majorLineWidth = shortEdge/300 - ZStack { - Path(outerRingPath) - .fill(.clear) - .background(Color(cgColor: backColor), in: Path(outerRingPath)) + + Path(outerRingPath) + .fill(.thickMaterial) + Path(outerRingPath) + .fill(Color(cgColor: backColor)) + + Canvas { graphicsContext, size in + var shadowContext = graphicsContext + shadowContext.clip(to: Path(outerRingPath), options: .inverse) + shadowContext.addFilter(.shadow(color: Color(white: 0, opacity: 0.5 * Double(tanh(shadowSize * 32))), radius: shadowSize * shortEdge, + x: -shadowSize / 2 * sin(CGFloat.pi * 2 * shadowDirection) * shortEdge, + y: -shadowSize / 2 * cos(CGFloat.pi * 2 * shadowDirection) * shortEdge, options: .shadowOnly)) + shadowContext.fill(Path(outerRing.path), with: .color(white: 1)) - Canvas { graphicsContext, size in - var context = graphicsContext - context.clip(to: Path(outerRingPath), style: FillStyle(eoFill: true)) - - var gradientContext = context - gradientContext.clipToLayer(options: .inverse) { ctx in - ctx.addFilter(.luminanceToAlpha) - ctx.fill(Path(roundedRect: CGRect(origin: .zero, size: size), cornerSize: .zero), with: .color(white: 0)) - ctx.clip(to: Path(textMaskPath), options: .inverse) - ctx.stroke(Path(majorTicksPath), with: .color(white: 1 - majorTickAlpha), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - ctx.clip(to: Path(minorTrackPath), style: FillStyle(eoFill: true)) - ctx.stroke(Path(minorTicksPath), with: .color(white: 1 - minorTickAlpha), style: StrokeStyle(lineWidth: minorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - } - - var inactiveRingContext = gradientContext - inactiveRingContext.clipToLayer(opacity: alpha) { cont in - cont.fill(Path(CGRect(origin: .zero, size: size)), with: .color(white: 1)) - } - inactiveRingContext.fill(Path(outerRingPath), with: .conicGradient(gradient, center: CGPoint(x: size.width/2, y: size.height/2), angle: Angle(degrees: 90))) - gradientContext.clip(to: Path(pathWithAngle)) - gradientContext.fill(Path(outerRingPath), with: .conicGradient(gradient, center: CGPoint(x: size.width/2, y: size.height/2), angle: Angle(degrees: 90))) - - var tickContext = context - tickContext.clip(to: Path(textMaskPath), options: .inverse) - tickContext.stroke(Path(majorTicksPath), with: .color(Color(cgColor: majorTickColor)), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - tickContext.clip(to: Path(minorTrackPath), style: FillStyle(eoFill: true)) - tickContext.stroke(Path(minorTicksPath), with: .color(Color(cgColor: minorTickColor)), style: StrokeStyle(lineWidth: minorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - - var transform = CGAffineTransform() - var textContext = context - for drawabeText in drawableTexts { - if drawabeText.transform != transform { - textContext = context - textContext.concatenate(drawabeText.transform) - } - textContext.draw(Text(drawabeText.string).foregroundColor(Color(cgColor: drawabeText.color)), in: drawabeText.position) - transform = drawabeText.transform - } - - for drawableMark in drawableMarks { - var markContext = context - markContext.addFilter(.shadow(color: Color(white: 0, opacity: 0.5), radius: drawableMark.radius/2, x: 0, y: 0)) - markContext.fill(Path(drawableMark.path), with: .color(Color(cgColor: drawableMark.color))) + var context = graphicsContext + context.clip(to: Path(outerRingPath), style: FillStyle(eoFill: true)) + + var gradientContext = context + gradientContext.clipToLayer(options: .inverse) { ctx in + ctx.addFilter(.luminanceToAlpha) + ctx.fill(Path(roundedRect: CGRect(origin: .zero, size: size), cornerSize: .zero), with: .color(white: 0)) + ctx.clip(to: Path(textMaskPath), options: .inverse) + ctx.stroke(Path(majorTicksPath), with: .color(white: 1 - majorTickAlpha), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) + ctx.clip(to: Path(minorTrackPath), style: FillStyle(eoFill: true)) + ctx.stroke(Path(minorTicksPath), with: .color(white: 1 - minorTickAlpha), style: StrokeStyle(lineWidth: minorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) + } + + var inactiveRingContext = gradientContext + inactiveRingContext.clipToLayer(opacity: alpha) { cont in + cont.fill(Path(CGRect(origin: .zero, size: size)), with: .color(white: 1)) + } + inactiveRingContext.fill(Path(outerRingPath), with: .conicGradient(gradient, center: CGPoint(x: size.width/2, y: size.height/2), angle: startingAngle)) + gradientContext.clip(to: Path(pathWithAngle)) + gradientContext.fill(Path(outerRingPath), with: .conicGradient(gradient, center: CGPoint(x: size.width/2, y: size.height/2), angle: startingAngle)) + + var tickContext = context + tickContext.clip(to: Path(textMaskPath), options: .inverse) + tickContext.stroke(Path(majorTicksPath), with: .color(Color(cgColor: majorTickColor)), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) + tickContext.clip(to: Path(minorTrackPath), style: FillStyle(eoFill: true)) + tickContext.stroke(Path(minorTicksPath), with: .color(Color(cgColor: minorTickColor)), style: StrokeStyle(lineWidth: minorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) + + var transform = CGAffineTransform() + var textContext = context + for drawabeText in drawableTexts { + if drawabeText.transform != transform { + textContext = context + textContext.concatenate(drawabeText.transform) } - - var shadowContext = graphicsContext - shadowContext.clip(to: Path(outerRing.path), options: .inverse) - shadowContext.drawLayer { ctx in - ctx.addFilter(.shadow(color: Color(white: 0, opacity: 0.5 * Double(tanh(shadowSize * 32))), radius: shadowSize * shortEdge, - x: -shadowSize / 2 * sin(CGFloat.pi * 2 * shadowDirection) * shortEdge, - y: -shadowSize / 2 * cos(CGFloat.pi * 2 * shadowDirection) * shortEdge, options: .shadowOnly)) - ctx.fill(Path(outerRing.path), with: .color(white: 1)) + textContext.draw(Text(drawabeText.string).foregroundColor(Color(cgColor: drawabeText.color)), in: drawabeText.position) + transform = drawabeText.transform + } + + for drawableMark in drawableMarks { + var markContext = context + markContext.addFilter(.shadow(color: Color(white: 0, opacity: 0.5), radius: drawableMark.radius/2, x: 0, y: 0)) + markContext.fill(Path(drawableMark.path), with: .color(Color(cgColor: drawableMark.color))) + } + } + if highlightType != .none { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + Canvas { context, size in + context.fill(Path(outerRingPath), with: .conicGradient(highlightGradient, center: CGPoint(x: size.width/2, y: size.height/2), angle: highlightAngle)) } + .blendMode(.hardLight) + .blur(radius: width * shortEdge * 0.4) + .opacity(highlightType == .flicker ? Double(Int(timeline.date.timeIntervalSince1970) % 2) : 1.0) + .animation(.easeInOut(duration: 0.5), value: timeline.date) } } } @@ -303,40 +332,37 @@ struct Core: View { } var body: some View { - ZStack { - Path(outerBoundPath) - .fill(.clear) - .background(Color(cgColor: backColor), in: Path(outerBoundPath)) + Path(outerBoundPath) + .fill(.thickMaterial) + Path(outerBoundPath) + .fill(Color(cgColor: backColor)) + + Canvas { context, _ in + var shadowContext = context + shadowContext.clip(to: Path(outerBoundPath), options: .inverse) + shadowContext.addFilter(.shadow(color: Color(white: 0, opacity: 0.5 * Double(tanh(shadowSize * 32))), radius: shadowSize * shortEdge, + x: -shadowSize / 2 * sin(CGFloat.pi * 2 * shadowDirection) * shortEdge, + y: -shadowSize / 2 * cos(CGFloat.pi * 2 * shadowDirection) * shortEdge, options: .shadowOnly)) + shadowContext.fill(Path(outerBoundPath), with: .color(white: 1)) - Canvas { context, _ in - context.fill(Path(outerBoundPath), with: .color(Color(cgColor: innerColor))) - - var startPoint = CGPoint(x: viewSize.width/2, y: viewSize.height/2) - var endPoint = startPoint + context.fill(Path(outerBoundPath), with: .color(Color(cgColor: innerColor))) + + var startPoint = CGPoint(x: viewSize.width/2, y: viewSize.height/2) + var endPoint = startPoint - var textContext = context - textContext.clipToLayer { ctx in - for text in drawableTexts { - let resolved = ctx.resolve(Text(text.string)) - ctx.draw(resolved, at: CGPoint(x: text.position.midX, y: text.position.midY)) - startPoint.x = min(startPoint.x, text.position.minX) - startPoint.y = max(startPoint.y, text.position.maxY) - endPoint.x = max(endPoint.x, text.position.maxX) - endPoint.y = min(endPoint.y, text.position.minY) - } - } - - textContext.fill(Path(outerBoundPath), with: .linearGradient(gradient, startPoint: startPoint, endPoint: endPoint)) - - var shadowContext = context - shadowContext.clip(to: Path(outerBoundPath), options: .inverse) - shadowContext.drawLayer { ctx in - ctx.addFilter(.shadow(color: Color(white: 0, opacity: 0.5 * Double(tanh(shadowSize * 32))), radius: shadowSize * shortEdge, - x: -shadowSize / 2 * sin(CGFloat.pi * 2 * shadowDirection) * shortEdge, - y: -shadowSize / 2 * cos(CGFloat.pi * 2 * shadowDirection) * shortEdge, options: .shadowOnly)) - ctx.fill(Path(outerBoundPath), with: .color(white: 1)) + var textContext = context + textContext.clipToLayer { ctx in + for text in drawableTexts { + let resolved = ctx.resolve(Text(text.string)) + ctx.draw(resolved, at: CGPoint(x: text.position.midX, y: text.position.midY)) + startPoint.x = min(startPoint.x, text.position.minX) + startPoint.y = max(startPoint.y, text.position.maxY) + endPoint.x = max(endPoint.x, text.position.maxX) + endPoint.y = min(endPoint.y, text.position.minY) } } + + textContext.fill(Path(outerBoundPath), with: .linearGradient(gradient, startPoint: startPoint, endPoint: endPoint)) } } } diff --git a/Shared/Views/WatchFaceView.swift b/Shared/Views/WatchFaceView.swift index 47b960c..9c3fd03 100644 --- a/Shared/Views/WatchFaceView.swift +++ b/Shared/Views/WatchFaceView.swift @@ -7,24 +7,23 @@ import SwiftUI -struct ScaleEffectScale: EnvironmentKey { - static let defaultValue: CGFloat = 0 +struct DirectedScale: Equatable { + let value: CGFloat + let anchor: UnitPoint + init(value: CGFloat = 0, anchor: UnitPoint = .center) { + self.value = value + self.anchor = anchor + } } -struct ScaleEffectAnchor: EnvironmentKey { - static let defaultValue = UnitPoint.center +struct ScaleEffectKey: EnvironmentKey { + static let defaultValue = DirectedScale() } extension EnvironmentValues { - - var scaleEffectScale: CGFloat { - get { self[ScaleEffectScale.self] } - set { self[ScaleEffectScale.self] = newValue } - } - - var scaleEffectAnchor: UnitPoint { - get { self[ScaleEffectAnchor.self] } - set { self[ScaleEffectAnchor.self] = newValue } + var directedScale: DirectedScale { + get { self[ScaleEffectKey.self] } + set { self[ScaleEffectKey.self] = newValue } } } @@ -81,6 +80,54 @@ private func ringMarks(for ring: Rings, watchLayout: WatchLayout, chineseCalenda } } +struct PressState { + private var startTime: Date? + private var _startLocation: CGPoint? + var ended = false + var location: CGPoint? { + didSet { + if (_startLocation == nil && location != nil) || (_startLocation != nil && location == nil) { + _startLocation = location + } + } + } + var distance: CGFloat? { + if let location = location, let startLocation = _startLocation { + let translation = location - startLocation + return sqrt(pow(translation.x, 2) + pow(translation.y, 2)) + } else { + return nil + } + } + var duration: TimeInterval? { + if let startTime = startTime, !ended { + startTime.distance(to: .now) + } else { + nil + } + } + var tapped: Bool { + if let distance = distance, let duration = duration { + return distance < 12 && duration < 0.3 + } else { + return false + } + } + var pressing: Bool { + get { + startTime != nil && !ended + } set { + if newValue { + if startTime == nil { + startTime = .now + } + } else { + startTime = nil + } + } + } +} + func pressAnchor(pos: CGPoint?, size: CGSize, proxy: GeometryProxy) -> UnitPoint { let center = CGPointMake(size.width / 2, size.height / 2) let tapPosition: CGPoint @@ -106,8 +153,7 @@ struct Watch: View { #else let showsWidgetContainerBackground = true #endif - @Environment(\.scaleEffectScale) var scaleEffectScale - @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + @Environment(\.directedScale) var directedScale let shrink: Bool let displayZeroRing: Bool let displaySubquarter: Bool @@ -120,8 +166,9 @@ struct Watch: View { let centerOffset: CGFloat let entityNotes: EntityNotes? let shift: CGSize + let highlightType: HighlightType - init(displaySubquarter: Bool, displaySolarTerms: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, textShift: Bool = false, shrink: Bool = true) { + init(displaySubquarter: Bool, displaySolarTerms: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, highlightType: HighlightType, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, textShift: Bool = false, shrink: Bool = true) { self.shrink = shrink self.displayZeroRing = displaySolarTerms self.displaySubquarter = displaySubquarter @@ -132,6 +179,7 @@ struct Watch: View { self.chineseCalendar = chineseCalendar self.centerOffset = centerOffset self.entityNotes = entityNotes + self.highlightType = highlightType self.shift = if textShift { CGSizeMake(watchLayout.horizontalTextOffset, watchLayout.verticalTextOffset) } else { @@ -182,22 +230,22 @@ struct Watch: View { ZeroRing(width: ZeroRing.width * widthScale, viewSize: size, compact: compact, textFont: WatchFont(watchLayout.textFont), outerRing: outerBound, startingAngle: phase.zeroRing, oddTicks: chineseCalendar.oddSolarTerms.map { CGFloat($0) }, evenTicks: chineseCalendar.evenSolarTerms.map { CGFloat($0) }, oddColor: oddSTColor, evenColor: evenSTColor, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese, offset: shift) } let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, offset: shift) - .scaleEffect(1 + scaleEffectScale * 0.25, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: scaleEffectScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, offset: shift) - .scaleEffect(1 + scaleEffectScale * 0.5, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.65, blendDuration: 0.2), value: scaleEffectScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, offset: shift) - .scaleEffect(1 + scaleEffectScale * 0.75, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: scaleEffectScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, offset: shift) - .scaleEffect(1 + scaleEffectScale, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.75, blendDuration: 0.2), value: scaleEffectScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType, offset: shift) + .scaleEffect(1 + directedScale.value * 0.25, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.65, blendDuration: 0.2), value: directedScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + .scaleEffect(1 + directedScale.value, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.75, blendDuration: 0.2), value: directedScale) let timeString = displaySubquarter ? chineseCalendar.timeString : (chineseCalendar.hourString + chineseCalendar.shortQuarterString) Core(viewSize: size, compact: compact, dateString: chineseCalendar.dateString, timeString: timeString, font: WatchFont(watchLayout.centerFont), maxLength: 5, textColor: watchLayout.centerFontColor, outerBound: innerBound, innerColor: coreColor, backColor: backColor, centerOffset: centerOffset, shadowDirection: shadowDirection, shadowSize: watchLayout.shadowSize) - .scaleEffect(1 + scaleEffectScale * 1.25, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: scaleEffectScale) + .scaleEffect(1 + directedScale.value * 1.25, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: directedScale) } } } @@ -206,8 +254,7 @@ struct Watch: View { struct DateWatch: View { static let frameOffset: CGFloat = 0.03 - @Environment(\.scaleEffectScale) var scaleEffectScale - @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + @Environment(\.directedScale) var directedScale @Environment(\.colorScheme) var colorScheme #if !os(visionOS) @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground @@ -225,8 +272,9 @@ struct DateWatch: View { let chineseCalendar: ChineseCalendar let centerOffset: CGFloat let entityNotes: EntityNotes? + let highlightType: HighlightType - init(displaySolarTerms: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, shrink: Bool = true) { + init(displaySolarTerms: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, highlightType: HighlightType, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, shrink: Bool = true) { self.shrink = shrink self.displayZeroRing = displaySolarTerms self.compact = compact @@ -236,6 +284,7 @@ struct DateWatch: View { self.chineseCalendar = chineseCalendar self.centerOffset = centerOffset self.entityNotes = entityNotes + self.highlightType = highlightType } var body: some View { @@ -277,16 +326,16 @@ struct DateWatch: View { ZeroRing(width: ZeroRing.width * widthScale, viewSize: size, compact: compact, textFont: WatchFont(watchLayout.textFont), outerRing: outerBound, startingAngle: phase.zeroRing, oddTicks: chineseCalendar.oddSolarTerms.map { CGFloat($0) }, evenTicks: chineseCalendar.evenSolarTerms.map { CGFloat($0) }, oddColor: oddSTColor, evenColor: evenSTColor, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese) } let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0) - .scaleEffect(1 + scaleEffectScale * 0.5, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: scaleEffectScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize) - .scaleEffect(1 + scaleEffectScale * 0.75, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: scaleEffectScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) + .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) + .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) Core(viewSize: size, compact: compact, dateString: chineseCalendar.monthString, timeString: chineseCalendar.dayString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, innerColor: coreColor, backColor: backColor, centerOffset: centerOffset, shadowDirection: shadowDirection, shadowSize: watchLayout.shadowSize) - .scaleEffect(1 + scaleEffectScale, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: scaleEffectScale) + .scaleEffect(1 + directedScale.value, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: directedScale) } } } @@ -295,8 +344,7 @@ struct DateWatch: View { struct TimeWatch: View { static let frameOffset: CGFloat = 0.03 - @Environment(\.scaleEffectScale) var scaleEffectScale - @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + @Environment(\.directedScale) var directedScale @Environment(\.colorScheme) var colorScheme #if !os(visionOS) @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground @@ -315,8 +363,9 @@ struct TimeWatch: View { let chineseCalendar: ChineseCalendar let centerOffset: CGFloat let entityNotes: EntityNotes? + let highlightType: HighlightType - init(matchZeroRingGap: Bool, displaySubquarter: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, shrink: Bool = true) { + init(matchZeroRingGap: Bool, displaySubquarter: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, highlightType: HighlightType, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, shrink: Bool = true) { self.shrink = shrink self.displayZeroRing = matchZeroRingGap self.compact = compact @@ -327,6 +376,7 @@ struct TimeWatch: View { self.chineseCalendar = chineseCalendar self.centerOffset = centerOffset self.entityNotes = entityNotes + self.highlightType = highlightType } var body: some View { @@ -364,17 +414,17 @@ struct TimeWatch: View { ZStack { let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0) - .scaleEffect(1 + scaleEffectScale * 0.5, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: scaleEffectScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize) - .scaleEffect(1 + scaleEffectScale * 0.75, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: scaleEffectScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) + .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) + .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) let timeString = displaySubquarter ? chineseCalendar.quarterString : chineseCalendar.shortQuarterString Core(viewSize: size, compact: compact, dateString: chineseCalendar.hourString, timeString: timeString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, innerColor: coreColor, backColor: backColor, centerOffset: centerOffset, shadowDirection: shadowDirection, shadowSize: watchLayout.shadowSize) - .scaleEffect(1 + scaleEffectScale, anchor: scaleEffectAnchor) - .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: scaleEffectScale) + .scaleEffect(1 + directedScale.value, anchor: directedScale.anchor) + .animation(.spring(duration: 0.5, bounce: 0.8, blendDuration: 0.2), value: directedScale) } } } diff --git a/Vision/Layout.swift b/Vision/Layout.swift index 822f690..54aa4ad 100644 --- a/Vision/Layout.swift +++ b/Vision/Layout.swift @@ -92,12 +92,21 @@ import Observation @Observable class WatchSetting { static let shared = WatchSetting() + enum Selection: String, CaseIterable { + case datetime, location, ringColor, decoration, markColor, layout + } + enum TabSelection: String, CaseIterable { + case spaceTime, design, themes, documentation + } var displayTime: Date? = nil var timezone: TimeZone? = nil var vertical = true var settingIsOpen = false var timeDisplay = "" + @ObservationIgnored var previousSelectionSpaceTime: Selection? = nil + @ObservationIgnored var previousSelectionDesign: Selection? = nil + @ObservationIgnored var previousTabSelection: TabSelection? = nil private init() {} diff --git a/Vision/Views/Setting.swift b/Vision/Views/Setting.swift index a007d06..b58dc1b 100644 --- a/Vision/Views/Setting.swift +++ b/Vision/Views/Setting.swift @@ -6,60 +6,142 @@ // import SwiftUI +import StoreKit struct Setting: View { @Environment(\.watchLayout) var watchLayout @Environment(\.watchSetting) var watchSetting @Environment(\.modelContext) private var modelContext + @Environment(\.requestReview) var requestReview + @State private var selection: WatchSetting.Selection? + @State private var selectedTab: WatchSetting.TabSelection = .spaceTime + let spaceTimePages = [WatchSetting.Selection.datetime, WatchSetting.Selection.location] + let designPages = [WatchSetting.Selection.ringColor, WatchSetting.Selection.decoration, WatchSetting.Selection.markColor, WatchSetting.Selection.layout] var body: some View { - TabView { - NavigationStack { - Datetime() - } - .tabItem { - Label("日時", systemImage: "clock") + TabView(selection: $selectedTab) { + NavigationSplitView { + List(selection: $selection) { + ForEach(spaceTimePages, id: \.self) { selection in + buildView(selection: selection) + } } - NavigationStack { - Location() - } - .tabItem { - Label("經緯度", systemImage: "location") + .navigationTitle("時空") + .task(id: selection) { + if selection == .none || !spaceTimePages.contains(selection!) { + selection = watchSetting.previousSelectionSpaceTime ?? .datetime + } else { + watchSetting.previousSelectionSpaceTime = selection + } } - NavigationStack { - RingSetting() - } - .tabItem { - Label("輪色", systemImage: "pencil.and.outline") + } detail: { + switch selection { + case .datetime: + NavigationStack { + Datetime() + } + case .location: + NavigationStack { + Location() + } + default: + EmptyView() } - NavigationStack { - ColorSetting() } - .tabItem { - Label("色塊", systemImage: "wand.and.stars") - } - NavigationStack { - LayoutSetting() + .tag(WatchSetting.TabSelection.spaceTime) + .tabItem { + Label("時空", systemImage: "globe.desk") } - .tabItem { - Label("佈局", systemImage: "square.resize") + .navigationSplitViewColumnWidth(ideal: 200) + + NavigationSplitView { + List(selection: $selection) { + ForEach(designPages, id: \.self) { selection in + buildView(selection: selection) + } } + .navigationTitle("設計") + .task(id: selection) { + if selection == .none || !designPages.contains(selection!) { + selection = watchSetting.previousSelectionDesign ?? .ringColor + } else { + watchSetting.previousSelectionDesign = selection + } + } + } detail: { + switch selection { + case .ringColor: + NavigationStack { + RingSetting() + } + case .decoration: + NavigationStack { + DecorationSetting() + } + case .markColor: + NavigationStack { + ColorSetting() + } + case .layout: + NavigationStack { + LayoutSetting() + } + default: + EmptyView() + } + } + .tag(WatchSetting.TabSelection.design) + .tabItem { + Label("設計", systemImage: "paintbrush") + } + .navigationSplitViewColumnWidth(ideal: 200) + NavigationStack { ThemesList() } - .tabItem { - Label("主題庫", systemImage: "archivebox") - } + .tag(WatchSetting.TabSelection.themes) + .tabItem { + Label("主題庫", systemImage: "archivebox") + } NavigationStack { Documentation() } - .tabItem { - Label("註釋", systemImage: "doc.questionmark") - } + .tag(WatchSetting.TabSelection.documentation) + .tabItem { + Label("註釋", systemImage: "doc.questionmark") + } + } + .task { + selectedTab = watchSetting.previousTabSelection ?? .spaceTime + } + .task(id: selectedTab) { + watchSetting.previousTabSelection = selectedTab } .onDisappear { + watchSetting.settingIsOpen = false watchLayout.saveDefault(context: modelContext) + if ThemeData.experienced() { + requestReview() + } + } + } + + func buildView(selection: WatchSetting.Selection) -> some View { + let sel = switch selection { + case .datetime: + Label("日時", systemImage: "clock") + case .location: + Label("經緯度", systemImage: "location") + case .ringColor: + Label("輪色", systemImage: "pencil.and.outline") + case .decoration: + Label("裝飾", systemImage: "paintpalette") + case .markColor: + Label("色塊", systemImage: "wand.and.stars") + case .layout: + Label("佈局", systemImage: "square.resize") } + return sel } } diff --git a/Vision/Views/WatchFace.swift b/Vision/Views/WatchFace.swift index 12f9304..0f5188f 100644 --- a/Vision/Views/WatchFace.swift +++ b/Vision/Views/WatchFace.swift @@ -18,32 +18,21 @@ struct WatchFace: View { @State var entityPresenting = EntitySelection() @State var tapPos: CGPoint? = nil @State var hoverBounds: CGRect = .zero - @GestureState var longPressed = false + @State var touchState = PressState() - func tapGesture(proxy: GeometryProxy, size: CGSize) -> some Gesture { - SpatialTapGesture(coordinateSpace: .local) - .onEnded { tap in - tapPos = tap.location - var tapPosition = tap.location - tapPosition.x -= (proxy.size.width - size.width) / 2 - tapPosition.y -= (proxy.size.height - size.height) / 2 - entityPresenting.activeNote = [] - for mark in entityPresenting.entityNotes.entities { - let diff = tapPosition - mark.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { - entityPresenting.activeNote.append(mark) - } - } - } - } - - var longPress: some Gesture { - LongPressGesture(minimumDuration: 0.5) - .updating($longPressed) { currentState, gestureState, - transaction in - gestureState = currentState + func tapped(tapPosition: CGPoint, proxy: GeometryProxy, size: CGSize) { + var tapPosition = tapPosition + tapPos = tapPosition + tapPosition.x -= (proxy.size.width - size.width) / 2 + tapPosition.y -= (proxy.size.height - size.height) / 2 + entityPresenting.activeNote = [] + for mark in entityPresenting.entityNotes.entities { + let diff = tapPosition - mark.position + let dist = sqrt(diff.x * diff.x + diff.y * diff.y) + if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { + entityPresenting.activeNote.append(mark) } + } } func mainSize(proxy: GeometryProxy) -> CGSize { @@ -75,15 +64,25 @@ struct WatchFace: View { } else { watchLayout.centerTextHOffset } + let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local) + .onChanged { value in + touchState.pressing = true + touchState.location = value.location + } + .onEnded { value in + if touchState.tapped { + tapped(tapPosition: touchState.location!, proxy: proxy, size: size) + } + touchState.pressing = false + touchState.location = nil + } ZStack { - Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) + Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, highlightType: .flicker, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) .frame(width: size.width, height: size.height) .position(CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) - .environment(\.scaleEffectScale, longPressed ? -0.1 : 0.0) - .environment(\.scaleEffectAnchor, pressAnchor(pos: tapPos, size: size, proxy: proxy)) - .gesture(longPress) - .simultaneousGesture(tapGesture(proxy: proxy, size: size)) + .environment(\.directedScale, DirectedScale(value: touchState.pressing ? -0.1 : 0.0, anchor: pressAnchor(pos: touchState.location, size: size, proxy: proxy))) + .gesture(gesture) Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) } diff --git a/Vision/layout.txt b/Vision/layout.txt index 8c14c1d..e0ab7b3 100644 --- a/Vision/layout.txt +++ b/Vision/layout.txt @@ -1,5 +1,6 @@ globalMonth: false apparentTime: false +largeHour: false locationEnabled: true backAlpha: 1.0 firstRing: locations: 0.0, 0.25, 0.5, 0.75, 0.875; colors: 0xFF95517A, 0xFF9060BC, 0xFF6E68E7, 0xFF00A6FF, 0xFF7556EF; loop: true diff --git a/Vision/visionApp.swift b/Vision/visionApp.swift index 0991a00..c9ca2b5 100644 --- a/Vision/visionApp.swift +++ b/Vision/visionApp.swift @@ -82,7 +82,7 @@ struct Chinendar: App { watchSetting.timeDisplay = String(statusBar(from: chineseCalendar, options: watchLayout).reversed()) } } - .defaultSize(width: 700, height: 700) + .defaultSize(width: 900, height: 700) } func statusBar(from chineseCalendar: ChineseCalendar, options watchLayout: WatchLayout) -> String { diff --git a/Watch/Views/DateTimeAdjust.swift b/Watch/Views/DateTimeAdjust.swift index eb4ac4e..b3e95ab 100644 --- a/Watch/Views/DateTimeAdjust.swift +++ b/Watch/Views/DateTimeAdjust.swift @@ -54,12 +54,12 @@ struct DateTimeAdjust: View { DatePicker(selection: $timeManager.time, in: ChineseCalendar.start...ChineseCalendar.end, displayedComponents: [.date]) { Text("日", comment: "Date") } - .animation(.default, value: timeManager.time) + .animation(.smooth, value: timeManager.time) .minimumScaleFactor(0.75) DatePicker(selection: $timeManager.time, displayedComponents: [.hourAndMinute]) { Text("時", comment: "Time") } - .animation(.default, value: timeManager.time) + .animation(.smooth, value: timeManager.time) .minimumScaleFactor(0.75) } .toolbar { diff --git a/Watch/Views/WatchFace.swift b/Watch/Views/WatchFace.swift index 48369a7..5c3b78f 100644 --- a/Watch/Views/WatchFace.swift +++ b/Watch/Views/WatchFace.swift @@ -12,41 +12,43 @@ struct WatchFace: View { @Binding var entityPresenting: EntitySelection @State var tapPos: CGPoint? = nil @State var hoverBounds: CGRect = .zero - @GestureState var longPressed = false @ViewBuilder let content: () -> Content + @State var touchState = PressState() - var tapGesture: some Gesture { - SpatialTapGesture(coordinateSpace: .local) - .onEnded { tap in - tapPos = tap.location - let tapPosition = tap.location - entityPresenting.activeNote = [] - for mark in entityPresenting.entityNotes.entities { - let diff = tapPosition - mark.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < 30 { - entityPresenting.activeNote.append(mark) - } - } - } - } - - var longPress: some Gesture { - LongPressGesture(minimumDuration: 3) - .updating($longPressed) { currentState, gestureState, - transaction in - gestureState = currentState + func tapped(tapPosition: CGPoint, proxy: GeometryProxy, size: CGSize) { + var tapPosition = tapPosition + tapPos = tapPosition + tapPosition.x -= (proxy.size.width - size.width) / 2 + tapPosition.y -= (proxy.size.height - size.height) / 2 + entityPresenting.activeNote = [] + for mark in entityPresenting.entityNotes.entities { + let diff = tapPosition - mark.position + let dist = sqrt(diff.x * diff.x + diff.y * diff.y) + if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { + entityPresenting.activeNote.append(mark) } + } } var body: some View { GeometryReader { proxy in + let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local) + .onChanged { value in + touchState.pressing = true + touchState.location = value.location + } + .onEnded { value in + if touchState.tapped { + tapped(tapPosition: touchState.location!, proxy: proxy, size: proxy.size) + } + touchState.pressing = false + touchState.location = nil + } + ZStack { content() - .environment(\.scaleEffectScale, longPressed ? -0.1 : 0.0) - .environment(\.scaleEffectAnchor, pressAnchor(pos: tapPos, size: proxy.size, proxy: proxy)) - .gesture(longPress) - .simultaneousGesture(tapGesture) + .environment(\.directedScale, DirectedScale(value: touchState.pressing ? -0.1 : 0.0, anchor: pressAnchor(pos: touchState.location, size: proxy.size, proxy: proxy))) + .gesture(gesture) Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) } .animation(.easeInOut(duration: 0.2), value: tapPos) @@ -64,7 +66,7 @@ struct WatchFaceDate: View { var body: some View { WatchFace(entityPresenting: $entityPresenting) { DateWatch(displaySolarTerms: false, compact: true, - watchLayout: watchLayout, markSize: 1.5, chineseCalendar: chineseCalendar, widthScale: 1.5, entityNotes: entityPresenting.entityNotes, shrink: false) + watchLayout: watchLayout, markSize: 1.5, chineseCalendar: chineseCalendar, highlightType: .flicker, widthScale: 1.5, entityNotes: entityPresenting.entityNotes, shrink: false) .frame(width: watchSetting.size.width, height: watchSetting.size.height) } } @@ -79,7 +81,7 @@ struct WatchFaceTime: View { var body: some View { WatchFace(entityPresenting: $entityPresenting) { - TimeWatch(matchZeroRingGap: false, displaySubquarter: true, compact: true, watchLayout: watchLayout, markSize: 1.5, chineseCalendar: chineseCalendar, widthScale: 1.5, entityNotes: entityPresenting.entityNotes, shrink: false) + TimeWatch(matchZeroRingGap: false, displaySubquarter: true, compact: true, watchLayout: watchLayout, markSize: 1.5, chineseCalendar: chineseCalendar, highlightType: .flicker, widthScale: 1.5, entityNotes: entityPresenting.entityNotes, shrink: false) .frame(width: watchSetting.size.width, height: watchSetting.size.height) } } @@ -95,7 +97,7 @@ struct WatchFaceFull: View { var body: some View { WatchFace(entityPresenting: $entityPresenting) { Watch(displaySubquarter: true, displaySolarTerms: false, compact: true, - watchLayout: watchLayout, markSize: 1.3, chineseCalendar: chineseCalendar, entityNotes: entityPresenting.entityNotes, shrink: false) + watchLayout: watchLayout, markSize: 1.3, chineseCalendar: chineseCalendar, highlightType: .flicker, entityNotes: entityPresenting.entityNotes, shrink: false) .frame(width: watchSetting.size.width, height: watchSetting.size.height) } } diff --git a/Watch/layout.txt b/Watch/layout.txt index d76ab92..eaaf840 100644 --- a/Watch/layout.txt +++ b/Watch/layout.txt @@ -1,5 +1,6 @@ globalMonth: false apparentTime: false +largeHour: false locationEnabled: true dualWatch: false backAlpha: 1.0 diff --git a/Widget/Dual.swift b/Widget/Dual.swift index babe287..9ac9bae 100644 --- a/Widget/Dual.swift +++ b/Widget/Dual.swift @@ -110,11 +110,11 @@ struct MediumWidgetEntryView: View { HStack(spacing: (proxy.size.width - proxy.size.height * 2) * 0.5) { switch entry.configuration.order { case .timeFirst: - TimeWatch(matchZeroRingGap: isLarge, displaySubquarter: false, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: isLarge ? 1.1 : 1.5) - DateWatch(displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: isLarge ? 1.1 : 1.5) + TimeWatch(matchZeroRingGap: isLarge, displaySubquarter: false, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: isLarge ? 1.1 : 1.5) + DateWatch(displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: isLarge ? 1.1 : 1.5) case .dateFirst: - DateWatch(displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: isLarge ? 1.1 : 1.5) - TimeWatch(matchZeroRingGap: isLarge, displaySubquarter: false, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: isLarge ? 1.1 : 1.5) + DateWatch(displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: isLarge ? 1.1 : 1.5) + TimeWatch(matchZeroRingGap: isLarge, displaySubquarter: false, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: isLarge ? 1.1 : 1.5) } } .padding(.horizontal, (proxy.size.width - proxy.size.height * 2) * 0.25) diff --git a/Widget/Full.swift b/Widget/Full.swift index 8efafea..1c4e1a4 100644 --- a/Widget/Full.swift +++ b/Widget/Full.swift @@ -91,7 +91,7 @@ struct LargeWidgetEntryView: View { var body: some View { let isLarge = widgetFamily == .systemLarge - Watch(displaySubquarter: false, displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.0, chineseCalendar: entry.chineseCalendar, widthScale: isLarge ? 0.8 : 1.0) + Watch(displaySubquarter: false, displaySolarTerms: isLarge, compact: !isLarge, watchLayout: entry.watchLayout, markSize: 1.0, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: isLarge ? 0.8 : 1.0) .containerBackground(backColor(), for: .widget) .padding(5) } diff --git a/Widget/Single.swift b/Widget/Single.swift index 04dc7d5..02c2d79 100644 --- a/Widget/Single.swift +++ b/Widget/Single.swift @@ -111,11 +111,11 @@ struct SmallWidgetEntryView: View { var body: some View { switch entry.configuration.mode { case .time: - TimeWatch(matchZeroRingGap: false, displaySubquarter: false, compact: true, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: 1.5) + TimeWatch(matchZeroRingGap: false, displaySubquarter: false, compact: true, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: 1.5) .containerBackground(backColor, for: .widget) .padding(5) case .date: - DateWatch(displaySolarTerms: false, compact: true, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, widthScale: 1.5) + DateWatch(displaySolarTerms: false, compact: true, watchLayout: entry.watchLayout, markSize: 1.5, chineseCalendar: entry.chineseCalendar, highlightType: .alwaysOn, widthScale: 1.5) .containerBackground(backColor, for: .widget) .padding(5) } diff --git a/iOS/Views/Setting.swift b/iOS/Views/Setting.swift index 9c1ea37..304d1af 100644 --- a/iOS/Views/Setting.swift +++ b/iOS/Views/Setting.swift @@ -15,7 +15,7 @@ struct Setting: View { var body: some View { List { - Section(header: Text("數據", comment: "Data Source")) { + Section("時空") { NavigationLink { Datetime() } label: { @@ -57,53 +57,38 @@ struct Setting: View { } } - Section(header: Text("樣式", comment: "Styles")) { + Section("設計") { NavigationLink{ RingSetting() } label: { - Label { - Text("輪色", comment: "Rings Color Setting") - } icon: { - Image(systemName: "pencil.and.outline") - } + Label("輪色", systemImage: "pencil.and.outline") + } + NavigationLink { + DecorationSetting() + } label: { + Label("裝飾", systemImage: "paintpalette") } NavigationLink { ColorSetting() } label: { - Label { - Text("色塊", comment: "Mark Color settings") - } icon: { - Image(systemName: "wand.and.stars") - } + Label("色塊", systemImage: "wand.and.stars") } NavigationLink { LayoutSetting() } label: { - Label { - Text("佈局", comment: "Layout settings section") - } icon: { - Image(systemName: "square.resize") - } + Label("佈局", systemImage: "square.resize") } } - Section(header: Text("其它", comment: "Miscellaneous")) { + Section { NavigationLink { ThemesList() } label: { - Label { - Text("主題庫", comment: "manage saved themes") - } icon: { - Image(systemName: "archivebox") - } + Label("主題庫", systemImage: "archivebox") } NavigationLink { Documentation() } label: { - Label { - Text("註釋", comment: "Documentation View") - } icon: { - Image(systemName: "doc.questionmark") - } + Label("註釋", systemImage: "doc.questionmark") } } } diff --git a/iOS/Views/WatchFace.swift b/iOS/Views/WatchFace.swift index 00d14d3..00794bd 100644 --- a/iOS/Views/WatchFace.swift +++ b/iOS/Views/WatchFace.swift @@ -7,6 +7,7 @@ import SwiftUI import WidgetKit +import StoreKit @MainActor struct WatchFace: View { @@ -15,11 +16,14 @@ struct WatchFace: View { @Environment(\.watchSetting) var watchSetting @Environment(\.scenePhase) var scenePhase @Environment(\.modelContext) private var modelContext + @Environment(\.requestReview) var requestReview @State var showWelcome = false @State var entityPresenting = EntitySelection() - @State var tapPos: CGPoint? = nil + @State var touchState = PressState() @State var hoverBounds: CGRect = .zero - @GestureState var longPressed = false + @State var tapPos: CGPoint? + @State var timer: Timer? + @GestureState private var dragging = false var presentSetting: Binding { .init(get: { watchSetting.presentSetting }, set: { newValue in @@ -28,38 +32,27 @@ struct WatchFace: View { watchLayout.saveDefault(context: modelContext) WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) - } - }) - } - - func tapGesture(proxy: GeometryProxy, size: CGSize) -> some Gesture { - SpatialTapGesture(coordinateSpace: .local) - .onEnded { tap in - tapPos = tap.location - var tapPosition = tap.location - tapPosition.x -= (proxy.size.width - size.width) / 2 - tapPosition.y -= (proxy.size.height - size.height) / 2 - entityPresenting.activeNote = [] - for mark in entityPresenting.entityNotes.entities { - let diff = tapPosition - mark.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { - entityPresenting.activeNote.append(mark) - } + + if ThemeData.experienced() { + requestReview() } } + }) } - var longPress: some Gesture { - LongPressGesture(minimumDuration: 0.5) - .updating($longPressed) { currentState, gestureState, - transaction in - gestureState = currentState - } - .onEnded { finished in - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - watchSetting.presentSetting = finished + func tapped(tapPosition: CGPoint, proxy: GeometryProxy, size: CGSize) { + var tapPosition = tapPosition + tapPos = tapPosition + tapPosition.x -= (proxy.size.width - size.width) / 2 + tapPosition.y -= (proxy.size.height - size.height) / 2 + entityPresenting.activeNote = [] + for mark in entityPresenting.entityNotes.entities { + let diff = tapPosition - mark.position + let dist = sqrt(diff.x * diff.x + diff.y * diff.y) + if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { + entityPresenting.activeNote.append(mark) } + } } func mainSize(proxy: GeometryProxy) -> CGSize { @@ -90,18 +83,45 @@ struct WatchFace: View { } else { watchLayout.centerTextHOffset } + let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local) + .updating($dragging) { _, state, _ in + state = true + } + .onChanged { value in + touchState.pressing = true + touchState.location = value.location + } ZStack { - Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) + Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, highlightType: .flicker, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) .frame(width: size.width, height: size.height) .position(CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) - .environment(\.scaleEffectScale, longPressed ? -0.1 : 0.0) - .environment(\.scaleEffectAnchor, pressAnchor(pos: tapPos, size: size, proxy: proxy)) - .gesture(longPress) - .simultaneousGesture(tapGesture(proxy: proxy, size: size)) + .environment(\.directedScale, DirectedScale(value: touchState.pressing ? -0.1 : 0.0, anchor: pressAnchor(pos: touchState.location, size: size, proxy: proxy))) + .gesture(gesture) Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) } + .onChange(of: dragging) { _, newValue in + if newValue { + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in + Task { @MainActor in + if !watchSetting.presentSetting { + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + watchSetting.presentSetting = true + touchState.ended = true + } + } + } + } else { + timer?.invalidate() + if touchState.tapped { + tapped(tapPosition: touchState.location!, proxy: proxy, size: size) + } + touchState.pressing = false + touchState.location = nil + touchState.ended = false + } + } .onChange(of: proxy.size) { watchSetting.vertical = proxy.size.height >= proxy.size.width } diff --git a/iOS/layout.txt b/iOS/layout.txt index 66da124..cb8edf5 100644 --- a/iOS/layout.txt +++ b/iOS/layout.txt @@ -1,5 +1,6 @@ globalMonth: false apparentTime: false +largeHour: false locationEnabled: true backAlpha: 1.0 firstRing: locations: 0.0, 0.25, 0.5, 0.75, 0.875; colors: 0xFF95517A, 0xFF9060BC, 0xFF6E68E7, 0xFF00A6FF, 0xFF7556EF; loop: true diff --git a/macOS/Layout.swift b/macOS/Layout.swift index bc4220b..1f18c95 100644 --- a/macOS/Layout.swift +++ b/macOS/Layout.swift @@ -101,7 +101,7 @@ import Observation @Observable class WatchSetting { static let shared = WatchSetting() enum Selection: String, CaseIterable { - case datetime, location, ringColor, markColor, layout, themes, documentation + case datetime, location, ringColor, decoration, markColor, layout, themes, documentation } var displayTime: Date? = nil diff --git a/macOS/Views/Setting.swift b/macOS/Views/Setting.swift index 9d67c56..58d7a60 100644 --- a/macOS/Views/Setting.swift +++ b/macOS/Views/Setting.swift @@ -6,7 +6,8 @@ // import SwiftUI -@preconcurrency import WidgetKit +import WidgetKit +import StoreKit struct Setting: View { @Environment(\.watchLayout) var watchLayout @@ -16,6 +17,7 @@ struct Setting: View { @State private var selection: WatchSetting.Selection? = .none @State private var columnVisibility = NavigationSplitViewVisibility.automatic @Environment(\.modelContext) private var modelContext + @Environment(\.requestReview) var requestReview private var statusState: StatusState { StatusState(locationManager: locationManager, watchLayout: watchLayout, watchSetting: watchSetting) @@ -24,17 +26,19 @@ struct Setting: View { var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { List(selection: $selection) { - Section(header: Text("數據", comment: "Data Source")) { + Section("時空") { ForEach([WatchSetting.Selection.datetime, WatchSetting.Selection.location], id: \.self) { selection in buildView(selection: selection) } } - Section(header: Text("樣式", comment: "Styles")) { - ForEach([WatchSetting.Selection.ringColor, WatchSetting.Selection.markColor, WatchSetting.Selection.layout], id: \.self) { selection in + Section("設計") { + ForEach([WatchSetting.Selection.ringColor, + WatchSetting.Selection.decoration, + WatchSetting.Selection.markColor, WatchSetting.Selection.layout], id: \.self) { selection in buildView(selection: selection) } } - Section(header: Text("其它", comment: "Miscellaneous")) { + Section { ForEach([WatchSetting.Selection.themes, WatchSetting.Selection.documentation], id: \.self) { selection in buildView(selection: selection) } @@ -48,6 +52,8 @@ struct Setting: View { Location() case .ringColor: RingSetting() + case .decoration: + DecorationSetting() case .markColor: ColorSetting() case .layout: @@ -75,7 +81,12 @@ struct Setting: View { } } .navigationSplitViewStyle(.balanced) - .onChange(of: selection) { + .task(id: selection) { + if selection == .none { + selection = watchSetting.previousSelection ?? .datetime + } else { + watchSetting.previousSelection = selection + } cleanColorPanel() } .onChange(of: statusState) { @@ -84,10 +95,11 @@ struct Setting: View { } } .onAppear { - selection = watchSetting.previousSelection ?? .datetime + if ThemeData.experienced() { + requestReview() + } } .onDisappear { - watchSetting.previousSelection = selection selection = .none watchLayout.saveDefault(context: modelContext) WidgetCenter.shared.reloadAllTimelines() @@ -104,6 +116,8 @@ struct Setting: View { Label("經緯度", systemImage: "location") case .ringColor: Label("輪色", systemImage: "pencil.and.outline") + case .decoration: + Label("裝飾", systemImage: "paintpalette") case .markColor: Label("色塊", systemImage: "wand.and.stars") case .layout: diff --git a/macOS/Views/WatchFace.swift b/macOS/Views/WatchFace.swift index 2f17841..6959fbb 100644 --- a/macOS/Views/WatchFace.swift +++ b/macOS/Views/WatchFace.swift @@ -14,34 +14,23 @@ struct WatchFace: View { @State var entityPresenting = EntitySelection() @State var tapPos: CGPoint? = nil @State var hoverBounds: CGRect = .zero - @GestureState var longPressed = false + @State var touchState = PressState() @Environment(\.openWindow) private var openWindow @Environment(\.modelContext) private var modelContext - func tapGesture(proxy: GeometryProxy, size: CGSize) -> some Gesture { - SpatialTapGesture(coordinateSpace: .local) - .onEnded { tap in - tapPos = tap.location - var tapPosition = tap.location - tapPosition.x -= (proxy.size.width - size.width) / 2 - tapPosition.y -= (proxy.size.height - size.height) / 2 - entityPresenting.activeNote = [] - for mark in entityPresenting.entityNotes.entities { - let diff = tapPosition - mark.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { - entityPresenting.activeNote.append(mark) - } - } - } - } - - var longPress: some Gesture { - LongPressGesture(minimumDuration: 3) - .updating($longPressed) { currentState, gestureState, - transaction in - gestureState = currentState + func tapped(tapPosition: CGPoint, proxy: GeometryProxy, size: CGSize) { + var tapPosition = tapPosition + tapPos = tapPosition + tapPosition.x -= (proxy.size.width - size.width) / 2 + tapPosition.y -= (proxy.size.height - size.height) / 2 + entityPresenting.activeNote = [] + for mark in entityPresenting.entityNotes.entities { + let diff = tapPosition - mark.position + let dist = sqrt(diff.x * diff.x + diff.y * diff.y) + if dist.isFinite && dist < 5 * min(size.width, size.height) * Marks.markSize { + entityPresenting.activeNote.append(mark) } + } } var body: some View { @@ -53,14 +42,25 @@ struct WatchFace: View { watchLayout.centerTextHOffset } + let gesture = DragGesture(minimumDistance: 0, coordinateSpace: .local) + .onChanged { value in + touchState.pressing = true + touchState.location = value.location + } + .onEnded { value in + if touchState.tapped { + tapped(tapPosition: touchState.location!, proxy: proxy, size: size) + } + touchState.pressing = false + touchState.location = nil + } + ZStack { - Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) + Watch(displaySubquarter: true, displaySolarTerms: true, compact: false, watchLayout: watchLayout, markSize: 1.0, chineseCalendar: chineseCalendar, highlightType: .flicker, widthScale: 0.9, centerOffset: centerOffset, entityNotes: entityPresenting.entityNotes, textShift: true) .frame(width: size.width, height: size.height) .position(CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) - .environment(\.scaleEffectScale, longPressed ? -0.1 : 0.0) - .environment(\.scaleEffectAnchor, pressAnchor(pos: tapPos, size: size, proxy: proxy)) - .gesture(longPress) - .simultaneousGesture(tapGesture(proxy: proxy, size: size)) + .environment(\.directedScale, DirectedScale(value: touchState.pressing ? -0.1 : 0.0, anchor: pressAnchor(pos: touchState.location, size: size, proxy: proxy))) + .gesture(gesture) Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) } diff --git a/macOS/layout.txt b/macOS/layout.txt index 23f0916..a6fa120 100644 --- a/macOS/layout.txt +++ b/macOS/layout.txt @@ -1,5 +1,6 @@ globalMonth: false apparentTime: false +largeHour: false locationEnabled: true backAlpha: 1.0 firstRing: locations: 0.0, 0.25, 0.5, 0.75, 0.875; colors: 0xFF95517A, 0xFF9060BC, 0xFF6E68E7, 0xFF00A6FF, 0xFF7556EF; loop: true diff --git a/macOS/watchPanel.swift b/macOS/watchPanel.swift index 416fc74..6795412 100644 --- a/macOS/watchPanel.swift +++ b/macOS/watchPanel.swift @@ -142,7 +142,9 @@ internal final class WatchPanelHosting: Watc } @objc func openSetting(_ sender: NSButton) { - if settingWindow == nil { + if settingWindow == nil || !settingWindow!.isVisible { + settingWindow?.close() + settingWindow = nil settingView.sceneBridgingOptions = [.all] settingWindow = NSWindow(contentViewController: settingView) settingWindow?.styleMask = [.closable, .resizable, .titled, .unifiedTitleAndToolbar, .fullSizeContentView]