diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..d10e0bf --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,18 @@ +project: Chinendar.xcodeproj +retain_objc_accessible: true +schemes: +- Chinendar Mac +- Chinendar Vision +- Chinendar Watch +- Chinendar iOS +- Mac Widget Extension +- Watch Widget Extension +- iOS Widget Extension +targets: +- Chinendar Mac +- Chinendar Vision +- Chinendar Watch +- Chinendar iOS +- Mac Widget Extension +- Watch Widget Extension +- iOS Widget Extension diff --git a/Chinendar.xcodeproj/project.pbxproj b/Chinendar.xcodeproj/project.pbxproj index 2fe97c6..470fe19 100644 --- a/Chinendar.xcodeproj/project.pbxproj +++ b/Chinendar.xcodeproj/project.pbxproj @@ -161,6 +161,11 @@ B3BEB4C62A489A99000751D5 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; B3BFA2572A05E0590018F99E /* WatchConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */; }; B3BFA2582A05E0590018F99E /* WatchConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */; }; + B3C68B192B5DDC4B00FC08E3 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B182B5DDC4B00FC08E3 /* Card.swift */; }; + B3C68B1A2B5DDC4B00FC08E3 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B182B5DDC4B00FC08E3 /* Card.swift */; }; + B3C68B1C2B5DE90800FC08E3 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B1B2B5DE90800FC08E3 /* Protocols.swift */; }; + B3C68B1D2B5DE90800FC08E3 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B1B2B5DE90800FC08E3 /* Protocols.swift */; }; + B3C68B1E2B5DE94300FC08E3 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B1B2B5DE90800FC08E3 /* Protocols.swift */; }; B3CC8B9C2A0B30BB0063DE44 /* iOSWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8B9B2A0B30BB0063DE44 /* iOSWidgetBundle.swift */; }; B3CC8BA12A0B30BC0063DE44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */; }; B3CC8BA72A0B30BC0063DE44 /* Chinendar Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8B972A0B30BB0063DE44 /* Chinendar Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -366,6 +371,8 @@ 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 = ""; }; + B3C68B182B5DDC4B00FC08E3 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; + B3C68B1B2B5DE90800FC08E3 /* Protocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protocols.swift; sourceTree = ""; }; B3CC8B972A0B30BB0063DE44 /* Chinendar Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Chinendar Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; B3CC8B9B2A0B30BB0063DE44 /* iOSWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSWidgetBundle.swift; sourceTree = ""; }; B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -601,6 +608,7 @@ B383A6D02A4D02E2002FADCF /* Dual.swift */, B3CC8BB62A0B330C0063DE44 /* Full.swift */, 9E57427E2AA501E70052AE70 /* TaskGroup.swift */, + B3C68B1B2B5DE90800FC08E3 /* Protocols.swift */, B3E8A5142A4CF64A00302473 /* WatchWidgets */, ); path = Widget; @@ -635,6 +643,7 @@ B3E8A5152A4CF67700302473 /* Circular.swift */, B3E8A5182A4CF6BD00302473 /* CountDown.swift */, B3CC8BF22A0C7E300063DE44 /* TextDesp.swift */, + B3C68B182B5DDC4B00FC08E3 /* Card.swift */, B3E8A51B2A4CF77000302473 /* Corner.swift */, B32243E42A0D8B6C00E7AED5 /* WatchWidgetBasic.swift */, B395B5A02A0ED7EF003206E7 /* IconView.swift */, @@ -1091,8 +1100,10 @@ files = ( B3CC8BB32A0B31BE0063DE44 /* MetaLayout.swift in Sources */, B395B5A82A0F22CF003206E7 /* IconView.swift in Sources */, + B3C68B192B5DDC4B00FC08E3 /* Card.swift in Sources */, 9E5742812AA504D20052AE70 /* TaskGroup.swift in Sources */, B3CC8BB02A0B31B70063DE44 /* Model.swift in Sources */, + B3C68B1C2B5DE90800FC08E3 /* Protocols.swift in Sources */, B34009482A352FEA003F50F7 /* WatchFaceView.swift in Sources */, B3CC8BB12A0B31B90063DE44 /* PlanetModel.swift in Sources */, B383A6CF2A4D02D8002FADCF /* Single.swift in Sources */, @@ -1126,6 +1137,7 @@ B3E8A51A2A4CF6BD00302473 /* CountDown.swift in Sources */, B38E96B62A0D3AA0002FD662 /* MetaLayout.swift in Sources */, B395B5A62A0F1A4A003206E7 /* IconView.swift in Sources */, + B3C68B1A2B5DDC4B00FC08E3 /* Card.swift in Sources */, B32243F32A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B38E96B42A0D3A8C002FD662 /* Data.swift in Sources */, B32999322A4F9C8700B71579 /* LocationManager.swift in Sources */, @@ -1133,6 +1145,7 @@ B32243E82A0D8E5000E7AED5 /* WatchWidgetBundle.swift in Sources */, B383A6D62A4D1C7B002FADCF /* Relevance.swift in Sources */, B32243E52A0D8B6C00E7AED5 /* WatchWidgetBasic.swift in Sources */, + B3C68B1D2B5DE90800FC08E3 /* Protocols.swift in Sources */, 9E5742822AA504D30052AE70 /* TaskGroup.swift in Sources */, B3E8A5172A4CF67700302473 /* Circular.swift in Sources */, B3CC8BF32A0C7E300063DE44 /* TextDesp.swift in Sources */, @@ -1159,6 +1172,7 @@ B3CC8BBD2A0B40E00063DE44 /* Full.swift in Sources */, B34009472A352FEA003F50F7 /* WatchFaceView.swift in Sources */, B3E1D6DE2A0AC89300F2905A /* Model.swift in Sources */, + B3C68B1E2B5DE94300FC08E3 /* Protocols.swift in Sources */, B3E1D6DF2A0AC89600F2905A /* PlanetModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1675,8 +1689,7 @@ D2E4E0F326F7C73F002F3716 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 113; + APP_BUILD = 117; APP_VERSION = 5.3; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; @@ -1734,11 +1747,13 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_OUTPUT_FORMAT = binary; IPHONEOS_DEPLOYMENT_TARGET = 17.0; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + PLIST_FILE_OUTPUT_FORMAT = binary; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = binary; @@ -1754,8 +1769,7 @@ D2E4E0F426F7C73F002F3716 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 113; + APP_BUILD = 117; APP_VERSION = 5.3; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; @@ -1808,12 +1822,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_OUTPUT_FORMAT = binary; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = NO; + PLIST_FILE_OUTPUT_FORMAT = binary; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = binary; diff --git a/Chinendar.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Chinendar.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Chinendar.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Chinendar.xcodeproj/project.xcworkspace/xcuserdata/leo.xcuserdatad/WorkspaceSettings.xcsettings b/Chinendar.xcodeproj/project.xcworkspace/xcuserdata/leo.xcuserdatad/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..bbfef02 --- /dev/null +++ b/Chinendar.xcodeproj/project.xcworkspace/xcuserdata/leo.xcuserdatad/WorkspaceSettings.xcsettings @@ -0,0 +1,14 @@ + + + + + BuildLocationStyle + UseAppPreferences + CustomBuildLocationType + RelativeToDerivedData + DerivedDataLocationStyle + Default + ShowSharedSchemesAutomaticallyEnabled + + + diff --git a/Chinendar.xcodeproj/xcshareddata/xcschemes/Chinendar Vision.xcscheme b/Chinendar.xcodeproj/xcshareddata/xcschemes/Chinendar Vision.xcscheme index eb6eb5c..7edbe33 100644 --- a/Chinendar.xcodeproj/xcshareddata/xcschemes/Chinendar Vision.xcscheme +++ b/Chinendar.xcodeproj/xcshareddata/xcschemes/Chinendar Vision.xcscheme @@ -49,6 +49,28 @@ ReferencedContainer = "container:Chinendar.xcodeproj"> + + + + + + + + + + + + { data in data.name == defaultName && data.deviceName == deviceName @@ -450,8 +450,8 @@ extension String { } func saveDefault(context: ModelContext) { - let defaultName = ThemeData.defaultName - let deviceName = ThemeData.deviceName + let defaultName = AppInfo.defaultName + let deviceName = AppInfo.deviceName try? LocalData.write(deviceName: deviceName) let predicate = #Predicate { data in @@ -474,7 +474,7 @@ extension String { } if !found { - let defaultTheme = ThemeData(name: ThemeData.defaultName, code: self.encode()) + let defaultTheme = ThemeData(name: AppInfo.defaultName, code: self.encode()) context.insert(defaultTheme) } } diff --git a/Shared/DataModel/ThemeData.swift b/Shared/DataModel/ThemeData.swift index 0772108..ac17002 100644 --- a/Shared/DataModel/ThemeData.swift +++ b/Shared/DataModel/ThemeData.swift @@ -18,13 +18,7 @@ import WatchKit import VisionKit #endif -typealias ThemeData = DataSchemaV2.Layout - -extension ThemeData: Identifiable, Hashable { - static var version: Int { - intVersion(DataSchemaV2.versionIdentifier) - } - +struct AppInfo { #if os(macOS) static let groupId = Bundle.main.object(forInfoDictionaryKey: "GroupID") as! String #elseif os(iOS) || os(visionOS) @@ -42,12 +36,18 @@ extension ThemeData: Identifiable, Hashable { #elseif os(visionOS) @MainActor static let deviceName = UIDevice.current.name #endif - - static let defaultName = NSLocalizedString("Default", comment: "Default save file name") + static let defaultName = "__current_theme__" +} + +typealias ThemeData = DataSchemaV3.Layout +extension ThemeData { + static var version: Int { + intVersion(DataSchemaV3.versionIdentifier) + } static let container = { - let fullSchema = Schema(versionedSchema: DataSchemaV2.self) - let baseUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ThemeData.groupId)! + let fullSchema = Schema(versionedSchema: DataSchemaV3.self) + let baseUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppInfo.groupId)! #if os(macOS) let url = baseUrl.appendingPathComponent("ChineseTime") #else @@ -57,16 +57,16 @@ extension ThemeData: Identifiable, Hashable { return createContainer(schema: fullSchema, migrationPlan: DataMigrationPlan.self, configurations: [modelConfig]) }() - static let context = ModelContext(ThemeData.container) + static let context = ModelContext(container) static func latestVersion() -> Int { - let deviceName = ThemeData.deviceName + let deviceName = AppInfo.deviceName let predicate = #Predicate { data in data.deviceName == deviceName && data.version != nil } var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) descriptor.fetchLimit = 1 - let version = try? ThemeData.context.fetch(descriptor).first?.version + let version = try? context.fetch(descriptor).first?.version return version ?? 0 } static func experienced() -> Bool { @@ -74,9 +74,9 @@ extension ThemeData: Identifiable, Hashable { data.modifiedDate != nil } var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate)]) - let counts = try? ThemeData.context.fetchCount(descriptor) + let counts = try? context.fetchCount(descriptor) descriptor.fetchLimit = 1 - let date = try? ThemeData.context.fetch(descriptor).first?.modifiedDate + let date = try? context.fetch(descriptor).first?.modifiedDate if let date = date, let counts = counts, counts > 1, date.distance(to: .now) > 3600 * 24 * 30 { return true @@ -93,7 +93,9 @@ extension ThemeData: Identifiable, Hashable { if self.code != code { self.code = code self.modifiedDate = Date.now - self.version = ThemeData.version + } + if (self.version ?? 0) < Self.version { + self.version = Self.version } } } @@ -115,6 +117,29 @@ private func createContainer(schema: Schema, migrationPlan: SchemaMigrationPlan. } } +enum DataSchemaV3: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(1, 2, 1) + static var models: [any PersistentModel.Type] { + [Layout.self] + } + + @Model final class Layout { + var code: String? + var deviceName: String? + var modifiedDate: Date? + @Attribute(hashModifier: "v3") var name: String? + var version: Int? + + init(name: String, code: String) { + self.name = name + self.deviceName = AppInfo.deviceName + self.code = code + self.modifiedDate = Date.now + self.version = intVersion(DataSchemaV3.versionIdentifier) + } + } +} + enum DataSchemaV2: VersionedSchema { static let versionIdentifier: Schema.Version = .init(1, 1, 1) static var models: [any PersistentModel.Type] { @@ -130,7 +155,7 @@ enum DataSchemaV2: VersionedSchema { init(name: String, code: String) { self.name = name - self.deviceName = Layout.deviceName + self.deviceName = AppInfo.deviceName self.code = code self.modifiedDate = Date.now self.version = intVersion(DataSchemaV2.versionIdentifier) @@ -161,12 +186,33 @@ enum DataSchemaV1: VersionedSchema { enum DataMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { - [DataSchemaV1.self, DataSchemaV2.self] + [DataSchemaV1.self, DataSchemaV2.self, DataSchemaV3.self] } - static var stages: [MigrationStage] { [migrateV1toV2] } + static var stages: [MigrationStage] { [migrateV1toV2, migrateV2toV3] } static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self) + static let migrateV2toV3 = MigrationStage.custom( + fromVersion: DataSchemaV2.self, toVersion: DataSchemaV3.self, + willMigrate: { context in + let legacyDefaultName = NSLocalizedString("Default", comment: "Legacy default theme name") + let deviceName = AppInfo.deviceName + let predicate = #Predicate { data in + data.name == legacyDefaultName && data.deviceName == deviceName + } + var descriptor = FetchDescriptor(predicate: predicate) + do { + let themes = try context.fetch(descriptor) + for theme in themes { + theme.name = AppInfo.defaultName + } + try context.save() + } catch { + print(error.localizedDescription) + } + }, + didMigrate: nil + ) } enum LocalSchemaV1: VersionedSchema { @@ -197,7 +243,7 @@ extension LocalData: Identifiable, Hashable { static let container = { let localSchema = Schema(versionedSchema: LocalSchemaV1.self) - let modelConfig = ModelConfiguration("ChineseTimeLocal", schema: localSchema, groupContainer: .identifier(ThemeData.groupId), cloudKitDatabase: .none) + let modelConfig = ModelConfiguration("ChineseTimeLocal", schema: localSchema, groupContainer: .identifier(AppInfo.groupId), cloudKitDatabase: .none) return createContainer(schema: localSchema, migrationPlan: nil, configurations: [modelConfig]) }() diff --git a/Shared/Localizable.xcstrings b/Shared/Localizable.xcstrings index a6b5100..a72b1d3 100644 --- a/Shared/Localizable.xcstrings +++ b/Shared/Localizable.xcstrings @@ -155,7 +155,7 @@ } }, "Default" : { - "comment" : "Default save file name", + "comment" : "Legacy default theme name", "localizations" : { "en" : { "stringUnit" : { @@ -455,7 +455,7 @@ "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. 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 亥." + "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 亥.
**Month names**: 1st-10th months are numerically named. 11th Month is 冬月 (Winter Month) since Winter Solstice is in this month, and 12th and last Month is 臘月 (Worship Month), for it's time to make major offerings to various spirits." } }, "ja" : { @@ -542,6 +542,35 @@ } } }, + "佚名" : { + "comment" : "unnamed", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New Theme" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "匿名" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "익명" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "佚名" + } + } + } + }, "作罷" : { "comment" : "Ok", "extractionState" : "manual", @@ -572,59 +601,58 @@ } } }, - "全錶" : { + "僅時" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Full Watch" + "value" : "Hour Only" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "完全な時計" + "value" : "時間のみ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "완전한 시계" + "value" : "시간만" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "全表" + "value" : "仅时" } } } }, - "其它" : { - "comment" : "Miscellaneous", + "全錶" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Miscellaneous" + "value" : "Full Watch" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "他の" + "value" : "完全な時計" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "다른" + "value" : "완전한 시계" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "其它" + "value" : "全表" } } } @@ -1738,7 +1766,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Complete Chinese Time watch face" + "value" : "Complete Chinendar watch face" } }, "ja" : { @@ -2419,6 +2447,34 @@ } } }, + "常用" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常用" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일반" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "常用" + } + } + } + }, "常駐狀態欄" : { "comment" : "Welcome, ring design - title", "localizations" : { @@ -2817,79 +2873,95 @@ "value" : "본문" } }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字" + } + } + } + }, + "文字片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text Card" + } + }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "情報元" + "value" : "文字カード" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "데이터" + "value" : "본문 카드" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "文字" + "value" : "文字片" } } } }, - "文字" : { + "日" : { + "comment" : "Date", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Text" + "value" : "Date" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "文字" + "value" : "日付" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "본문" + "value" : "일" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "文字" + "value" : "日" } } } }, - "日" : { - "comment" : "Date", + "日、節日" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Date" + "value" : "Date and Holiday" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日付" + "value" : "日付と祝日" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "일" + "value" : "날짜과 주일" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "日、节日" } } } @@ -2993,7 +3065,7 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日出と没" + "value" : "日出と日没" } }, "ko" : { @@ -3067,6 +3139,34 @@ } } }, + "日時、節日" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date, Time and Holiday" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日付、時間、祝日" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "날짜, 시간, 주일" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日时、节日" + } + } + } + }, "日時:" : { "comment" : "Date & time section", "localizations" : { @@ -3382,6 +3482,34 @@ } } }, + "時刻" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hour and Quarter" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時と刻" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시과 각" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时刻" + } + } + } + }, "時區" : { "comment" : "Timezone section", "localizations" : { @@ -3851,7 +3979,7 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "月出と没" + "value" : "月出と月没" } }, "ko" : { @@ -4213,102 +4341,6 @@ } } }, - "樸素寫就之華曆" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chinese Time in plain text" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "平易な言葉で書かれた華暦" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "일반 단어로 된 화력" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "様式" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "양식" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "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" : "次节气" - } - } - } - }, "樸素寫就之華曆" : { "localizations" : { "en" : { @@ -5758,18 +5790,6 @@ "value" : "색상표" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "記号の色" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "색상표" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5863,6 +5883,34 @@ } } }, + "華曆文字片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time Text on Card" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "華暦文字カード" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "화력 문자 카드" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历文字片" + } + } + } + }, "華曆片" : { "localizations" : { "en" : { @@ -6008,6 +6056,7 @@ } }, "裝飾" : { + "comment" : "Add on Setting", "localizations" : { "en" : { "stringUnit" : { @@ -6422,13 +6471,13 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "読点" + "value" : "区切り文字" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "반점" + "value" : "구분 기호" } }, "zh-Hans" : { @@ -6439,6 +6488,34 @@ } } }, + "讀號選項" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Separator Options" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "区切り文字の選択" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "구분 기호 선택" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "读号选项" + } + } + } + }, "距離次事件之倒計時" : { "localizations" : { "en" : { @@ -6545,18 +6622,6 @@ "value" : "륜의 색상" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "輪の色" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "륜의 색상" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7081,4 +7146,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Shared/Setting/Datetime.swift b/Shared/Setting/Datetime.swift index bba4d85..edbb872 100644 --- a/Shared/Setting/Datetime.swift +++ b/Shared/Setting/Datetime.swift @@ -34,12 +34,6 @@ fileprivate struct TimeZoneSelection: Equatable { } var tertiary: String - init(primary: String = "", secondary: String = "", tertiary: String = "") { - self.primary = primary - self.secondary = secondary - self.tertiary = tertiary - } - init(timezone: TimeZone) { let components = timezone.identifier.split(separator: "/") primary = if components.count > 0 { String(components[0]) } else { "" } diff --git a/Shared/Setting/Decoration.swift b/Shared/Setting/Decoration.swift index 4598d59..88c76c9 100644 --- a/Shared/Setting/Decoration.swift +++ b/Shared/Setting/Decoration.swift @@ -141,7 +141,7 @@ struct DecorationSetting: View { } } .formStyle(.grouped) - .navigationTitle(Text("輪色", comment: "Rings Color Setting")) + .navigationTitle(Text("裝飾", comment: "Add on Setting")) #if os(iOS) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Shared/Setting/RingSetting.swift b/Shared/Setting/RingSetting.swift index 9115f4f..0eadf3b 100644 --- a/Shared/Setting/RingSetting.swift +++ b/Shared/Setting/RingSetting.swift @@ -356,9 +356,6 @@ struct GradientSliderView: View { struct RingSetting: View { @Environment(\.watchLayout) var watchLayout @Environment(\.watchSetting) var watchSetting -#if os(macOS) - let observer = ColorPanelObserver() -#endif var body: some View { Form { diff --git a/Shared/Setting/ThemesList.swift b/Shared/Setting/ThemesList.swift index c81ca7e..54c76fe 100644 --- a/Shared/Setting/ThemesList.swift +++ b/Shared/Setting/ThemesList.swift @@ -77,11 +77,11 @@ struct ThemesList: View { private var themes: [String: [ThemeData]] { loadThemes(data: dataStack) } - let currentDeviceName = ThemeData.deviceName + let currentDeviceName = AppInfo.deviceName var body: some View { let newTheme = Button { - newName = validName(ThemeData.defaultName) + newName = validName(NSLocalizedString("佚名", comment: "unnamed")) createAlert = true } label: { Label("謄錄", systemImage: "square.and.pencil") @@ -158,20 +158,12 @@ struct ThemesList: View { #endif ForEach(themes[key]!, id: \.self) { theme in if !theme.isNil { - - let deleteButton = Button(role: .destructive) { - target = theme - deleteAlert = true - } label: { - Label("刪", systemImage: "trash") - } - - let renameButton = Button { - target = theme - newName = validName(theme.name!) - renameAlert = true - } label: { - Label("更名", systemImage: "rectangle.and.pencil.and.ellipsis.rtl") + 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) } let applyButton = Button { @@ -192,50 +184,91 @@ struct ThemesList: View { 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) - } - + if theme.name! != AppInfo.defaultName { + let deleteButton = Button(role: .destructive) { + target = theme + deleteAlert = true + } label: { + Label("刪", systemImage: "trash") + } + + let renameButton = Button { + target = theme + newName = validName(theme.name!) + renameAlert = true + } label: { + Label("更名", systemImage: "rectangle.and.pencil.and.ellipsis.rtl") + } #if os(macOS) - HStack { + HStack { + Menu { + applyButton + renameButton + saveButton + deleteButton + } label: { + Text(theme.name!) + } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.accessoryBar) + .labelStyle(.titleAndIcon) + Spacer() + dateLabel + } +#else Menu { applyButton renameButton saveButton deleteButton } label: { - Text(theme.name!) + HStack { + Text(theme.name!) + Spacer() + dateLabel + } } .menuIndicator(.hidden) .menuStyle(.button) - .buttonStyle(.accessoryBar) + .buttonStyle(.borderless) .labelStyle(.titleAndIcon) - Spacer() - dateLabel - } -#else - Menu { - applyButton - renameButton - saveButton - deleteButton - } label: { + .tint(.primary) +#endif + } else { +#if os(macOS) HStack { - Text(theme.name!) + Menu { + applyButton + saveButton + } label: { + Text("常用") + } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.accessoryBar) + .labelStyle(.titleAndIcon) Spacer() dateLabel } - } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.borderless) - .labelStyle(.titleAndIcon) - .tint(.primary) +#else + Menu { + applyButton + saveButton + } label: { + HStack { + Text("常用") + Spacer() + dateLabel + } + } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.borderless) + .labelStyle(.titleAndIcon) + .tint(.primary) #endif + } } } } @@ -367,7 +400,7 @@ struct ThemesList: View { let currentDeviceThemes = themes[deviceName] return currentDeviceThemes == nil || !(currentDeviceThemes!.map { $0.name }.contains(name)) } else { - return true + return false } } diff --git a/Shared/Utilities.swift b/Shared/Utilities.swift index fa3f74f..2d15e1d 100644 --- a/Shared/Utilities.swift +++ b/Shared/Utilities.swift @@ -53,21 +53,6 @@ final class MarkdownParser { } } -extension String { - var boldRanges: [Range] { - var ranges: [Range] = [] - var startIndex = self.startIndex - while startIndex < endIndex, let range = self[startIndex...].range(of: "**") { - startIndex = range.upperBound - if let range2 = self[startIndex...].range(of: "**") { - ranges.append(range.upperBound.. Bool { - return registry[element] != nil - } - var count: Int { offsprings.count } @@ -112,26 +93,6 @@ final class DataTree: CustomStringConvertible { return nil } } - - func index(of element: String) -> Int? { - return registry[element] - } - - subscript(index: Int) -> DataTree? { - if (0.. = 0.3...0.9 let step: CGFloat = 0.1 @@ -44,8 +43,6 @@ struct Setting: View { DateTimeAdjust() } Toggle(NSLocalizedString("分列日時", comment: "Split Date and Time"), isOn: dualWatch) - } header: { - Text("其它", comment: "Miscellaneous") } footer: { Text("更多設置請移步 iOS App,可於手機與手錶間自動同步", comment: "Hint for syncing between watch and phone") } diff --git a/Widget/Dual.swift b/Widget/Dual.swift index 9ac9bae..a5b5feb 100644 --- a/Widget/Dual.swift +++ b/Widget/Dual.swift @@ -38,44 +38,22 @@ struct MediumConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMi } } -struct MediumProvider: AppIntentTimelineProvider { +struct MediumProvider: ChinendarAppIntentTimelineProvider { typealias Entry = MediumEntry typealias Intent = MediumConfiguration let modelContext = ThemeData.context let locationManager = LocationManager.shared - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: context.family != .systemExtraLarge) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) + func compactCalendar(context: Context) -> Bool { + return context.family != .systemExtraLarge } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: context.family != .systemExtraLarge) - return Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: context.family != .systemExtraLarge) - var chineseCalendars = [chineseCalendar.copy] - for entryDate in chineseCalendar.nextQuarters(count: 10) { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) - return Timeline(entries: entries, policy: .atEnd) + + func nextEntryDates(chineseCalendar: ChineseCalendar, config: MediumConfiguration, context: Context) -> [Date] { + return chineseCalendar.nextQuarters(count: 10) } } -struct MediumEntry: TimelineEntry, ChineseTimeEntry { +struct MediumEntry: TimelineEntry, ChinendarEntry { let date: Date let configuration: MediumProvider.Intent let chineseCalendar: ChineseCalendar diff --git a/Widget/Full.swift b/Widget/Full.swift index 1c4e1a4..f36e7de 100644 --- a/Widget/Full.swift +++ b/Widget/Full.swift @@ -24,44 +24,22 @@ struct LargeConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMig } } -struct LargeProvider: AppIntentTimelineProvider { +struct LargeProvider: ChinendarAppIntentTimelineProvider { typealias Entry = LargeEntry typealias Intent = LargeConfiguration let modelContext = ThemeData.context let locationManager = LocationManager.shared - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: context.family != .systemLarge) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) + func compactCalendar(context: Context) -> Bool { + return context.family != .systemLarge } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: context.family != .systemLarge) - return Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: context.family != .systemLarge) - var chineseCalendars = [chineseCalendar.copy] - for entryDate in chineseCalendar.nextQuarters(count: 10) { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) - return Timeline(entries: entries, policy: .atEnd) + + func nextEntryDates(chineseCalendar: ChineseCalendar, config: LargeConfiguration, context: Context) -> [Date] { + return chineseCalendar.nextQuarters(count: 10) } } -struct LargeEntry: TimelineEntry, ChineseTimeEntry { +struct LargeEntry: TimelineEntry, ChinendarEntry { let date: Date let configuration: LargeProvider.Intent let chineseCalendar: ChineseCalendar diff --git a/Widget/Protocols.swift b/Widget/Protocols.swift new file mode 100644 index 0000000..2f9f250 --- /dev/null +++ b/Widget/Protocols.swift @@ -0,0 +1,63 @@ +// +// Protocols.swift +// Chinendar +// +// Created by Leo Liu on 1/21/24. +// + +import WidgetKit +import AppIntents +import SwiftData + +protocol ChinendarAppIntentTimelineProvider: AppIntentTimelineProvider where Entry: ChinendarEntry { + var modelContext: ModelContext { get } + var locationManager: LocationManager { get } + + func nextEntryDates(chineseCalendar: ChineseCalendar, config: Entry.Intent, context: Context) -> [Date] + func compactCalendar(context: Context) -> Bool +} + +extension ChinendarAppIntentTimelineProvider { + func placeholder(in context: Context) -> Entry { + let watchLayout = WatchLayout.shared + watchLayout.loadStatic() + let chineseCalendar = ChineseCalendar(time: .now, compact: compactCalendar(context: context)) + return Entry(configuration: Entry.Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) + } + + func snapshot(for configuration: Entry.Intent, in context: Context) async -> Entry { + let watchLayout = WatchLayout.shared + watchLayout.loadDefault(context: modelContext, local: true) + let location = await locationManager.getLocation() + let chineseCalendar = ChineseCalendar(location: location, compact: compactCalendar(context: context)) + let entry = Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) + return entry + } + + func timeline(for configuration: Entry.Intent, in context: Context) async -> Timeline { + let watchLayout = WatchLayout.shared + watchLayout.loadDefault(context: modelContext, local: true) + let location = await locationManager.getLocation() + + let chineseCalendar = ChineseCalendar(location: location, compact: compactCalendar(context: context)) + let originalChineseCalendar = chineseCalendar.copy + let entryDates = nextEntryDates(chineseCalendar: chineseCalendar, config: configuration, context: context) + + var chineseCalendars = [chineseCalendar.copy] + for entryDate in entryDates { + chineseCalendar.update(time: entryDate, location: location) + chineseCalendars.append(chineseCalendar.copy) + } + let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) +#if os(watchOS) + if context.family == .accessoryRectangular { + await updateCountDownRelevantIntents(chineseCalendar: originalChineseCalendar) + } +#endif + return Timeline(entries: entries, policy: .atEnd) + } + + func compactCalendar(context: Context) -> Bool { + return true + } +} diff --git a/Widget/Single.swift b/Widget/Single.swift index 02c2d79..089ec61 100644 --- a/Widget/Single.swift +++ b/Widget/Single.swift @@ -38,50 +38,23 @@ struct SmallConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMig } } -struct SmallProvider: AppIntentTimelineProvider { +struct SmallProvider: ChinendarAppIntentTimelineProvider { typealias Entry = SmallEntry typealias Intent = SmallConfiguration let modelContext = ThemeData.context let locationManager = LocationManager.shared - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: true) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: true) - return Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: true) - let entryDates = switch configuration.mode { + func nextEntryDates(chineseCalendar: ChineseCalendar, config: SmallConfiguration, context: Context) -> [Date] { + return switch config.mode { case .time: chineseCalendar.nextQuarters(count: 15) case .date: chineseCalendar.nextHours(count: 15) } - var chineseCalendars = [chineseCalendar.copy] - for entryDate in entryDates { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) - return Timeline(entries: entries, policy: .atEnd) } } -struct SmallEntry: TimelineEntry, ChineseTimeEntry { +struct SmallEntry: TimelineEntry, ChinendarEntry { let date: Date let configuration: SmallProvider.Intent let chineseCalendar: ChineseCalendar diff --git a/Widget/TaskGroup.swift b/Widget/TaskGroup.swift index 9545fb5..586c43e 100644 --- a/Widget/TaskGroup.swift +++ b/Widget/TaskGroup.swift @@ -8,12 +8,12 @@ import WidgetKit import AppIntents -protocol ChineseTimeEntry: Sendable { +protocol ChinendarEntry: Sendable { associatedtype Intent: WidgetConfigurationIntent init(configuration: Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) } -func generateEntries(chineseCalendars: [ChineseCalendar], watchLayout: WatchLayout, configuration: Intent) async -> [Entry] where Entry.Intent == Intent { +func generateEntries(chineseCalendars: [ChineseCalendar], watchLayout: WatchLayout, configuration: Intent) async -> [Entry] where Entry.Intent == Intent { var entries: [Entry] = [] await withTaskGroup(of: Entry.self) { group in for calendar in chineseCalendars { @@ -28,5 +28,3 @@ func generateEntries [Date] { + return chineseCalendar.nextQuarters(count: 12) + } + + func recommendations() -> [AppIntentRecommendation] { + return [ + AppIntentRecommendation(intent: Intent(), description: "華曆"), + ] + } +} + +struct CardEntry: TimelineEntry, ChinendarEntry { + let date: Date + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let relevance: TimelineEntryRelevance? + + init(configuration: CardProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + self.date = chineseCalendar.time + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + self.relevance = TimelineEntryRelevance(score: 5, duration: 144) + } +} + +struct CardEntryView: View { + var entry: CardProvider.Entry + + var body: some View { + let chineseCalendar = entry.chineseCalendar + CalendarBadge(dateString: chineseCalendar.dateString, timeString: chineseCalendar.hourString + chineseCalendar.shortQuarterString, color: applyGradient(gradient: entry.watchLayout.centerFontColor, startingAngle: 0), backGround: Color(cgColor: entry.watchLayout.innerColor)) + .containerBackground(Color(cgColor: entry.watchLayout.innerColor), for: .widget) + } +} + +struct DateCardWidget: Widget { + static let kind: String = "Date Card" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: CardProvider.Intent.self, provider: CardProvider()) { entry in + CardEntryView(entry: entry) + } + .contentMarginsDisabled() + .containerBackgroundRemovable() + .configurationDisplayName("華曆片") + .description("寫有華曆日時之片") + .supportedFamilies([.accessoryRectangular]) + } +} + +#Preview("Card", as: .accessoryRectangular, using: CardProvider.Intent()) { + DateCardWidget() +} timelineProvider: { + CardProvider() +} diff --git a/Widget/WatchWidgets/Circular.swift b/Widget/WatchWidgets/Circular.swift index a10f389..12bb720 100644 --- a/Widget/WatchWidgets/Circular.swift +++ b/Widget/WatchWidgets/Circular.swift @@ -34,46 +34,19 @@ struct CircularConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntent } } -struct CircularProvider: AppIntentTimelineProvider { +struct CircularProvider: ChinendarAppIntentTimelineProvider { typealias Entry = CircularEntry typealias Intent = CircularConfiguration let modelContext = ThemeData.context let locationManager = LocationManager.shared - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: true) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: true) - return Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: true) - let entryDates = switch configuration.mode { + func nextEntryDates(chineseCalendar: ChineseCalendar, config: CircularConfiguration, context: Context) -> [Date] { + return switch config.mode { case .monthDay: chineseCalendar.nextHours(count: 12) case .daylight: chineseCalendar.nextQuarters(count: 15) } - var chineseCalendars = [chineseCalendar.copy] - for entryDate in entryDates { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) - return Timeline(entries: entries, policy: .atEnd) } func recommendations() -> [AppIntentRecommendation] { @@ -140,7 +113,7 @@ private func moonTimes(times: [ChineseCalendar.NamedPosition?]) -> ((start: CGFl } } -struct CircularEntry: TimelineEntry, ChineseTimeEntry { +struct CircularEntry: TimelineEntry, ChinendarEntry { let date: Date let configuration: CircularProvider.Intent let chineseCalendar: ChineseCalendar diff --git a/Widget/WatchWidgets/CountDown.swift b/Widget/WatchWidgets/CountDown.swift index 3b61eca..7556178 100644 --- a/Widget/WatchWidgets/CountDown.swift +++ b/Widget/WatchWidgets/CountDown.swift @@ -9,36 +9,14 @@ import AppIntents import SwiftUI @preconcurrency import WidgetKit -struct CountDownProvider: AppIntentTimelineProvider { +struct CountDownProvider: ChinendarAppIntentTimelineProvider { typealias Entry = CountDownEntry typealias Intent = CountDownConfiguration let modelContext = ThemeData.context let locationManager = LocationManager.shared - - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: true) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: true) - return Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: true) - let originalChineseCalendar = chineseCalendar.copy - - let allTimes = switch configuration.target { + func nextEntryDates(chineseCalendar: ChineseCalendar, config: CountDownConfiguration, context: Context) -> [Date] { + let allTimes = switch config.target { case .moonriseSet: nextMoonTimes(chineseCalendar: chineseCalendar) case .sunriseSet: @@ -48,24 +26,11 @@ struct CountDownProvider: AppIntentTimelineProvider { case .solarTerms: nextSolarTerm(chineseCalendar: chineseCalendar) } - let entryDates = if allTimes.count > 0 { + return if allTimes.count > 0 { allTimes } else { [chineseCalendar.startOfNextDay] } - - var chineseCalendars = [chineseCalendar.copy] - for entryDate in entryDates { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) -#if os(watchOS) - if context.family == .accessoryRectangular { - await updateCountDownRelevantIntents(chineseCalendar: originalChineseCalendar) - } -#endif - return Timeline(entries: entries, policy: .atEnd) } func recommendations() -> [AppIntentRecommendation] { @@ -105,7 +70,7 @@ private func find(in dates: [ChineseCalendar.NamedDate], at date: Date) -> (Chin } } -struct CountDownEntry: TimelineEntry, ChineseTimeEntry { +struct CountDownEntry: TimelineEntry, ChinendarEntry { let date: Date let configuration: CountDownProvider.Intent let chineseCalendar: ChineseCalendar diff --git a/Widget/WatchWidgets/TextDesp.swift b/Widget/WatchWidgets/TextDesp.swift index 01ddc44..62ccd86 100644 --- a/Widget/WatchWidgets/TextDesp.swift +++ b/Widget/WatchWidgets/TextDesp.swift @@ -9,73 +9,87 @@ import AppIntents import SwiftUI @preconcurrency import WidgetKit +enum TextWidgetSeparator: String, AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") + case space = " ", dot = "・", none = "" + static var caseDisplayRepresentations: [TextWidgetSeparator : DisplayRepresentation] = [ + .none: .init(title: "無"), + .dot: .init(title: "・"), + .space: .init(title: "空格"), + ] +} + +enum TextWidgetTime: String, AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") + case none, hour, hourAndQuarter + static var caseDisplayRepresentations: [TextWidgetTime : DisplayRepresentation] = [ + .none: .init(title: "無"), + .hour: .init(title: "僅時"), + .hourAndQuarter: .init(title: "時刻"), + ] +} + struct TextConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { static let intentClassName = "SingleLineIntent" static var title: LocalizedStringResource = "文字" static var description = IntentDescription("簡單華曆文字") + + @Parameter(title: "日", default: true) + var date: Bool + @Parameter(title: "時", default: .hour) + var time: TextWidgetTime + @Parameter(title: "節日", default: 1, controlStyle: .stepper, inclusiveRange: (0, 2)) + var holidays: Int + @Parameter(title: "讀號", default: .dot) + var separator: TextWidgetSeparator } -struct TextProvider: AppIntentTimelineProvider { +struct TextProvider: ChinendarAppIntentTimelineProvider { typealias Intent = TextConfiguration typealias Entry = TextEntry let modelContext = ThemeData.context let locationManager = LocationManager.shared - func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: true) - return Entry(configuration: Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) - } - - func snapshot(for configuration: Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: true) - let entry = Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) - return entry - } - - func timeline(for configuration: Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared - watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - - let chineseCalendar = ChineseCalendar(location: location, compact: true) - let entryDates = switch context.family { - case .accessoryInline: - chineseCalendar.nextHours(count: 12) - case .accessoryRectangular: - chineseCalendar.nextQuarters(count: 12) - default: - [Date]() + func nextEntryDates(chineseCalendar: ChineseCalendar, config: TextConfiguration, context: Context) -> [Date] { + switch config.time { + case .hour, .none: + return chineseCalendar.nextHours(count: 12) + case .hourAndQuarter: + return chineseCalendar.nextQuarters(count: 12) } - - var chineseCalendars = [chineseCalendar.copy] - for entryDate in entryDates { - chineseCalendar.update(time: entryDate, location: location) - chineseCalendars.append(chineseCalendar.copy) - } - let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) - return Timeline(entries: entries, policy: .atEnd) } func recommendations() -> [AppIntentRecommendation] { + let datetimeHoliday = Intent() + let datetime = Intent() + datetime.holidays = 0 + datetime.time = .hourAndQuarter + let dateholiday = Intent() + dateholiday.time = .none return [ - AppIntentRecommendation(intent: Intent(), description: "華曆"), + AppIntentRecommendation(intent: datetimeHoliday, description: "日時、節日"), + AppIntentRecommendation(intent: datetime, description: "日時"), + AppIntentRecommendation(intent: dateholiday, description: "日、節日"), ] } } -struct TextEntry: TimelineEntry, ChineseTimeEntry { +struct TextEntry: TimelineEntry, ChinendarEntry { let date: Date + let displayDate: Bool + let displayTime: TextWidgetTime + let DisplayHolidays: Int + let separator: String let chineseCalendar: ChineseCalendar let watchLayout: WatchLayout let relevance: TimelineEntryRelevance? init(configuration: TextProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { - date = chineseCalendar.time + self.date = chineseCalendar.time + self.displayDate = configuration.date + self.displayTime = configuration.time + self.DisplayHolidays = configuration.holidays + self.separator = configuration.separator.rawValue self.chineseCalendar = chineseCalendar self.watchLayout = watchLayout self.relevance = TimelineEntryRelevance(score: 5, duration: 144) @@ -84,20 +98,10 @@ struct TextEntry: TimelineEntry, ChineseTimeEntry { struct TextEntryView: View { var entry: TextProvider.Entry - @Environment(\.widgetFamily) var family var body: some View { - switch family { - case .accessoryInline: - LineDescription(chineseCalendar: entry.chineseCalendar) - .containerBackground(Color.clear, for: .widget) - case .accessoryRectangular: - let chineseCalendar = entry.chineseCalendar - CalendarBadge(dateString: chineseCalendar.dateString, timeString: chineseCalendar.hourString + chineseCalendar.shortQuarterString, color: applyGradient(gradient: entry.watchLayout.centerFontColor, startingAngle: 0), backGround: Color(cgColor: entry.watchLayout.innerColor)) - .containerBackground(Color(cgColor: entry.watchLayout.innerColor), for: .widget) - default: - EmptyView() - } + LineDescription(chineseCalendar: entry.chineseCalendar, displayDate: entry.displayDate, displayTime: entry.displayTime, displayHolidays: entry.DisplayHolidays, separator: entry.separator) + .containerBackground(Color.clear, for: .widget) } } @@ -115,29 +119,8 @@ struct LineWidget: Widget { } } -struct DateCardWidget: Widget { - static let kind: String = "Date Card" - - var body: some WidgetConfiguration { - AppIntentConfiguration(kind: Self.kind, intent: TextProvider.Intent.self, provider: TextProvider()) { entry in - TextEntryView(entry: entry) - } - .contentMarginsDisabled() - .containerBackgroundRemovable() - .configurationDisplayName("華曆片") - .description("寫有華曆日時之片") - .supportedFamilies([.accessoryRectangular]) - } -} - #Preview("Inline", as: .accessoryInline, using: TextProvider.Intent()) { LineWidget() } timelineProvider: { TextProvider() } - -#Preview("Card", as: .accessoryRectangular, using: TextProvider.Intent()) { - DateCardWidget() -} timelineProvider: { - TextProvider() -} diff --git a/Widget/WatchWidgets/WatchWidgetBasic.swift b/Widget/WatchWidgets/WatchWidgetBasic.swift index 03d9f0c..325bf14 100644 --- a/Widget/WatchWidgets/WatchWidgetBasic.swift +++ b/Widget/WatchWidgets/WatchWidgetBasic.swift @@ -10,14 +10,26 @@ import SwiftUI struct LineDescription: View { let text: String - init(chineseCalendar: ChineseCalendar) { - var text = chineseCalendar.dateString - let holidays = chineseCalendar.holidays - for holiday in holidays[.. 0 { + let holidays = chineseCalendar.holidays + for holiday in holidays[..