diff --git a/Chinese Time.xcodeproj/project.pbxproj b/Chinese Time.xcodeproj/project.pbxproj index 997c404..0b8d4b1 100644 --- a/Chinese Time.xcodeproj/project.pbxproj +++ b/Chinese Time.xcodeproj/project.pbxproj @@ -3,38 +3,91 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ + 9E0438C82A8FD5D7007217A8 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; + 9E0438C92A8FD5D7007217A8 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; + 9E0438CA2A8FD5D7007217A8 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; + 9E0438CC2A8FD5D7007217A8 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; + 9E0438CD2A8FD5E0007217A8 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; + 9E0C5BBA2A9BCFDC00503CE0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */; }; + 9E0C5BBB2A9BCFDD00503CE0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */; }; + 9E21B0342A9BE14A00E59F99 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9E9FB80D2A9BE02200888FA3 /* Localizable.xcstrings */; }; + 9E21B0352A9BE14B00E59F99 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9E9FB80D2A9BE02200888FA3 /* Localizable.xcstrings */; }; + 9E21B0362A9BE14B00E59F99 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9E9FB80D2A9BE02200888FA3 /* Localizable.xcstrings */; }; + 9E5742802AA504D20052AE70 /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E57427E2AA501E70052AE70 /* TaskGroup.swift */; }; + 9E5742812AA504D20052AE70 /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E57427E2AA501E70052AE70 /* TaskGroup.swift */; }; + 9E5742822AA504D30052AE70 /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E57427E2AA501E70052AE70 /* TaskGroup.swift */; }; + 9E5A41212AA61FC300B470BE /* Relevance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D32A4D1C7B002FADCF /* Relevance.swift */; }; + 9E5A41222AA61FC400B470BE /* Relevance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D32A4D1C7B002FADCF /* Relevance.swift */; }; + 9E6E10CD2AA6410A004CEDBE /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E57427E2AA501E70052AE70 /* TaskGroup.swift */; }; + 9E6E10CE2AA6410A004CEDBE /* TaskGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E57427E2AA501E70052AE70 /* TaskGroup.swift */; }; + 9E71FD022A50BB0F00C9CA78 /* ThemesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328A2EE2A3D19A4002191F4 /* ThemesList.swift */; }; + 9E71FD032A50BB3900C9CA78 /* Datetime.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833432A46695000E36989 /* Datetime.swift */; }; + 9E71FD042A50BD2A00C9CA78 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833452A4739FD00E36989 /* Location.swift */; }; + 9E71FD072A50BF2E00C9CA78 /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E71FD062A50BF2E00C9CA78 /* WatchFace.swift */; }; + 9E820BE72A7DEA1700453389 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E820BE62A7DEA1700453389 /* Welcome.swift */; }; + 9E90D7EF2A9EABD100855F2C /* Environments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E90D7EE2A9EABD100855F2C /* Environments.swift */; }; + 9E90D7F02A9EABD100855F2C /* Environments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E90D7EE2A9EABD100855F2C /* Environments.swift */; }; + 9E90D7F12A9EABD100855F2C /* Environments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E90D7EE2A9EABD100855F2C /* Environments.swift */; }; + 9E9889EF2A79EABF0066414A /* watchPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9889EE2A79EABF0066414A /* watchPanel.swift */; }; + 9EBFBE332A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9EBFBE342A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9EBFBE352A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9EBFBE362A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9EBFBE372A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9EBFBE382A58A40900DC42AF /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; + 9ECDCA022A50B24800E11161 /* LayoutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833392A46685200E36989 /* LayoutSetting.swift */; }; + 9ECDCA032A50B6CB00E11161 /* ColorSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328333B2A46687D00E36989 /* ColorSetting.swift */; }; + 9ECDCA042A50B6CE00E11161 /* RingSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328333D2A4668B000E36989 /* RingSetting.swift */; }; + 9ECDCA052A50B6D100E11161 /* Documentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833412A46691800E36989 /* Documentation.swift */; }; + 9EE0B02A2A9BD5F600FC97D5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; + 9EE0B02B2A9BD5F700FC97D5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; + 9EE0B02C2A9BD5F700FC97D5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; + 9EE0B02D2A9BD5F800FC97D5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; + 9EE0B02E2A9BD5F800FC97D5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; B301073D2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; B301073E2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; B30107402A09A0A500D0A50C /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; - B32243E22A0D3BF600E7AED5 /* WatchLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E12A0D3BF600E7AED5 /* WatchLayout.swift */; }; - B32243E32A0D3BF600E7AED5 /* WatchLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E12A0D3BF600E7AED5 /* WatchLayout.swift */; }; - B32243E52A0D8B6C00E7AED5 /* WatchWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E42A0D8B6C00E7AED5 /* WatchWidgetView.swift */; }; + B32243E22A0D3BF600E7AED5 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E12A0D3BF600E7AED5 /* Layout.swift */; }; + B32243E32A0D3BF600E7AED5 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E12A0D3BF600E7AED5 /* Layout.swift */; }; + B32243E52A0D8B6C00E7AED5 /* WatchWidgetBasic.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E42A0D8B6C00E7AED5 /* WatchWidgetBasic.swift */; }; B32243E82A0D8E5000E7AED5 /* WatchWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E72A0D8E5000E7AED5 /* WatchWidgetBundle.swift */; }; - B32243E92A0D8EAD00E7AED5 /* WatchWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BF22A0C7E300063DE44 /* WatchWidget.swift */; }; - B32243EB2A0D8FD100E7AED5 /* WatchWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E42A0D8B6C00E7AED5 /* WatchWidgetView.swift */; }; + B32243EB2A0D8FD100E7AED5 /* WatchWidgetBasic.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E42A0D8B6C00E7AED5 /* WatchWidgetBasic.swift */; }; B32243EC2A0D917800E7AED5 /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B39086192A03522800943F2B /* layout.txt */; }; B32243F02A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; B32243F12A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; B32243F22A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; B32243F32A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; + B328333A2A46685200E36989 /* LayoutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833392A46685200E36989 /* LayoutSetting.swift */; }; + B328333C2A46687D00E36989 /* ColorSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328333B2A46687D00E36989 /* ColorSetting.swift */; }; + B328333E2A4668B000E36989 /* RingSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328333D2A4668B000E36989 /* RingSetting.swift */; }; + B32833402A4668EA00E36989 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328333F2A4668EA00E36989 /* Welcome.swift */; }; + B32833422A46691800E36989 /* Documentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833412A46691800E36989 /* Documentation.swift */; }; + B32833442A46695000E36989 /* Datetime.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833432A46695000E36989 /* Datetime.swift */; }; + B32833462A4739FD00E36989 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32833452A4739FD00E36989 /* Location.swift */; }; + B328A2EF2A3D19A4002191F4 /* ThemesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328A2EE2A3D19A4002191F4 /* ThemesList.swift */; }; B329909B296A1F7F00D246E9 /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B329909A296A1F7F00D246E9 /* layout.txt */; }; - B33CC9BC29FB583B00426C92 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B33CC9BE29FB583B00426C92 /* Main.storyboard */; }; + B32999222A4F96D600B71579 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999212A4F96D600B71579 /* Setting.swift */; }; + B32999242A4F989600B71579 /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; + B32999252A4F9AB100B71579 /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; + B32999262A4F9B2500B71579 /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; + B32999292A4F9B7B00B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B329992E2A4F9C8500B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B329992F2A4F9C8500B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B32999302A4F9C8600B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B32999312A4F9C8700B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B32999322A4F9C8700B71579 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; + B34009472A352FEA003F50F7 /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; + B34009482A352FEA003F50F7 /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; B34DA20929FDC0B200562449 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34DA20829FDC0B200562449 /* Utilities.swift */; }; B34DA20A29FDC0B200562449 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34DA20829FDC0B200562449 /* Utilities.swift */; }; - B34DA20C29FDFE3800562449 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34DA20B29FDFE3800562449 /* ViewController.swift */; }; B35097DF2A0AEAF3001AB3CE /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; - B35097E12A0AEC9F001AB3CE /* WidgetFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35097E02A0AEC9F001AB3CE /* WidgetFaceView.swift */; }; - B35097E22A0B1966001AB3CE /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; - B3515CEE29F6149500E6BCDC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE029F6149400E6BCDC /* ViewController.swift */; }; - B3515CEF29F6149500E6BCDC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3515CE129F6149400E6BCDC /* LaunchScreen.storyboard */; }; - B3515CF129F6149500E6BCDC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE329F6149500E6BCDC /* AppDelegate.swift */; }; + B3515CF129F6149500E6BCDC /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE329F6149500E6BCDC /* iOSApp.swift */; }; B3515CF329F6149500E6BCDC /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE529F6149500E6BCDC /* Layout.swift */; }; B3515CF629F6149500E6BCDC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3515CE829F6149500E6BCDC /* Assets.xcassets */; }; - B3515CFA29F6149500E6BCDC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CEC29F6149500E6BCDC /* SceneDelegate.swift */; }; B3515CFD29F6153E00E6BCDC /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B3515CFC29F6153E00E6BCDC /* layout.txt */; }; B3515CFF29F6169D00E6BCDC /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; B3515D0029F616A000E6BCDC /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; @@ -42,15 +95,20 @@ B36D2F7A2A0483F800005162 /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; B37063A329FAFF3300CC6E57 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; B37063A429FAFF3300CC6E57 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; - B37063A629FB088900CC6E57 /* MetaWatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A529FB088900CC6E57 /* MetaWatchFace.swift */; }; - B37063A729FB088900CC6E57 /* MetaWatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A529FB088900CC6E57 /* MetaWatchFace.swift */; }; - B37063A929FB0C4600CC6E57 /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A829FB0C4600CC6E57 /* WatchFace.swift */; }; + B383A6CE2A4D02D8002FADCF /* Single.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6CD2A4D02D8002FADCF /* Single.swift */; }; + B383A6CF2A4D02D8002FADCF /* Single.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6CD2A4D02D8002FADCF /* Single.swift */; }; + B383A6D12A4D02E2002FADCF /* Dual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D02A4D02E2002FADCF /* Dual.swift */; }; + B383A6D22A4D02E2002FADCF /* Dual.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D02A4D02E2002FADCF /* Dual.swift */; }; + B383A6D52A4D1C7B002FADCF /* Relevance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D32A4D1C7B002FADCF /* Relevance.swift */; }; + B383A6D62A4D1C7B002FADCF /* Relevance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6D32A4D1C7B002FADCF /* Relevance.swift */; }; + B383A6D82A4D1EA1002FADCF /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */; }; + B383A6ED2A4D1EA2002FADCF /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */; }; + B38CC0492A4F1F1600F4DB9F /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38CC0482A4F1F1600F4DB9F /* WatchFace.swift */; }; B38E96B12A0D3A82002FD662 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; B38E96B32A0D3A8A002FD662 /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B38E96B42A0D3A8C002FD662 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; - B38E96B52A0D3A97002FD662 /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; B38E96B62A0D3AA0002FD662 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; - B39086012A0314DD00943F2B /* ChineseTimeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086002A0314DD00943F2B /* ChineseTimeApp.swift */; }; + B39086012A0314DD00943F2B /* watchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086002A0314DD00943F2B /* watchApp.swift */; }; B39086032A0314DD00943F2B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086022A0314DD00943F2B /* ContentView.swift */; }; B39086052A0314DD00943F2B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B39086042A0314DD00943F2B /* Assets.xcassets */; }; B390860B2A0314DD00943F2B /* Chinese Time.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = B39085FE2A0314DD00943F2B /* Chinese Time.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -64,25 +122,17 @@ B390861A2A03522800943F2B /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B39086192A03522800943F2B /* layout.txt */; }; B395B5A62A0F1A4A003206E7 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B395B5A02A0ED7EF003206E7 /* IconView.swift */; }; B395B5A82A0F22CF003206E7 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B395B5A02A0ED7EF003206E7 /* IconView.swift */; }; - B395B5A92A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */; }; - B395B5AA2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */; }; - B395B5AB2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */; }; - B395B5AC2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */; }; - B3AF0B8E29FB658600C78081 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3AF0B8C29FB658600C78081 /* InfoPlist.strings */; }; - B3AF0B9129FB658600C78081 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3AF0B8F29FB658600C78081 /* Localizable.strings */; }; - B3AF0B9429FB658600C78081 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3AF0B9229FB658600C78081 /* InfoPlist.strings */; }; - B3AF0B9729FB658600C78081 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3AF0B9529FB658600C78081 /* Localizable.strings */; }; + B3970CF32A45066C0095F561 /* TextDesp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BF22A0C7E300063DE44 /* TextDesp.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 */; }; + B3BEB4C52A489A10000751D5 /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; + 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 */; }; - B3BFA25E2A06BBA60018F99E /* ChineseTime Watch App-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3BFA25C2A06BBA60018F99E /* ChineseTime Watch App-InfoPlist.strings */; }; - B3BFA2612A06BBA60018F99E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3BFA25F2A06BBA60018F99E /* Localizable.strings */; }; - B3CC8B982A0B30BB0063DE44 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6C82A0AB43E00F2905A /* WidgetKit.framework */; }; - B3CC8B992A0B30BB0063DE44 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6CA2A0AB43E00F2905A /* SwiftUI.framework */; }; B3CC8B9C2A0B30BB0063DE44 /* iOSWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8B9B2A0B30BB0063DE44 /* iOSWidgetBundle.swift */; }; B3CC8BA12A0B30BC0063DE44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */; }; - B3CC8BA72A0B30BC0063DE44 /* iOS Widget Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8B972A0B30BB0063DE44 /* iOS Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - B3CC8BAD2A0B31910063DE44 /* WidgetFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35097E02A0AEC9F001AB3CE /* WidgetFaceView.swift */; }; - B3CC8BAE2A0B31970063DE44 /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; + B3CC8BA72A0B30BC0063DE44 /* Chinese Time Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8B972A0B30BB0063DE44 /* Chinese Time Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B3CC8BAF2A0B31B10063DE44 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE529F6149500E6BCDC /* Layout.swift */; }; B3CC8BB02A0B31B70063DE44 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; B3CC8BB12A0B31B90063DE44 /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; @@ -90,39 +140,17 @@ B3CC8BB32A0B31BE0063DE44 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; B3CC8BB42A0B31C20063DE44 /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; B3CC8BB52A0B323E0063DE44 /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; - B3CC8BB72A0B330C0063DE44 /* Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BB62A0B330C0063DE44 /* Widget.swift */; }; + B3CC8BB72A0B330C0063DE44 /* Full.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BB62A0B330C0063DE44 /* Full.swift */; }; B3CC8BB82A0B333F0063DE44 /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B3515CFC29F6153E00E6BCDC /* layout.txt */; }; B3CC8BB92A0B334E0063DE44 /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; - B3CC8BBD2A0B40E00063DE44 /* Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BB62A0B330C0063DE44 /* Widget.swift */; }; - B3CC8BBF2A0BCB5E0063DE44 /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; - B3CC8BC02A0BCB5F0063DE44 /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; - B3CC8BC12A0BCB730063DE44 /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; - B3CC8BC72A0C7A090063DE44 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BC52A0C7A090063DE44 /* InfoPlist.strings */; }; - B3CC8BCA2A0C7A090063DE44 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BC82A0C7A090063DE44 /* Localizable.strings */; }; - B3CC8BCD2A0C7A090063DE44 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BCB2A0C7A090063DE44 /* InfoPlist.strings */; }; - B3CC8BD02A0C7A090063DE44 /* Chinese-Time-Watch-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BCE2A0C7A090063DE44 /* Chinese-Time-Watch-InfoPlist.strings */; }; - B3CC8BD62A0C7A1F0063DE44 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BD42A0C7A1F0063DE44 /* Localizable.strings */; }; - B3CC8BDD2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */; }; - B3CC8BDE2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */; }; - B3CC8BDF2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */; }; - B3CC8BE02A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */; }; - B3CC8BEF2A0C7E300063DE44 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6C82A0AB43E00F2905A /* WidgetKit.framework */; }; - B3CC8BF02A0C7E300063DE44 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6CA2A0AB43E00F2905A /* SwiftUI.framework */; }; - B3CC8BF32A0C7E300063DE44 /* WatchWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BF22A0C7E300063DE44 /* WatchWidget.swift */; }; + B3CC8BBD2A0B40E00063DE44 /* Full.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BB62A0B330C0063DE44 /* Full.swift */; }; + B3CC8BF32A0C7E300063DE44 /* TextDesp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BF22A0C7E300063DE44 /* TextDesp.swift */; }; B3CC8BF62A0C7E310063DE44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BF52A0C7E310063DE44 /* Assets.xcassets */; }; - B3CC8BFC2A0C7E310063DE44 /* Watch Widget Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8BEE2A0C7E300063DE44 /* Watch Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B3CC8BFC2A0C7E310063DE44 /* Chinese Time Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8BEE2A0C7E300063DE44 /* Chinese Time Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B3CE494F2A111971007905C1 /* SourceHanSansKR-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */; }; - B3D239442A0F461C00E506EB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3D239422A0F461C00E506EB /* InfoPlist.strings */; }; - B3D239472A0F461C00E506EB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B3D239452A0F461C00E506EB /* Localizable.strings */; }; - B3D318DD2A1661C1009FC18D /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; - B3D318DE2A1661C2009FC18D /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; - B3D318DF2A1661C2009FC18D /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; - B3D318E02A1661C3009FC18D /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; - B3E1D6C92A0AB43E00F2905A /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6C82A0AB43E00F2905A /* WidgetKit.framework */; }; - B3E1D6CB2A0AB43E00F2905A /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E1D6CA2A0AB43E00F2905A /* SwiftUI.framework */; }; B3E1D6CE2A0AB43E00F2905A /* MacWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6CD2A0AB43E00F2905A /* MacWidgetBundle.swift */; }; B3E1D6D12A0AB43E00F2905A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3E1D6D02A0AB43E00F2905A /* Assets.xcassets */; }; - B3E1D6D82A0AB43E00F2905A /* Mac Widget Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3E1D6C62A0AB43E00F2905A /* Mac Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B3E1D6D82A0AB43E00F2905A /* Chinese Time Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3E1D6C62A0AB43E00F2905A /* Chinese Time Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B3E1D6DD2A0AC88B00F2905A /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; B3E1D6DE2A0AC89300F2905A /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; B3E1D6DF2A0AC89600F2905A /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; @@ -131,16 +159,22 @@ B3E1D6E22A0AC91700F2905A /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; B3E1D6E42A0ACD7800F2905A /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; B3E1D6E52A0ACDD800F2905A /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26CF1BF26FD0C8D004EE9BB /* Layout.swift */; }; - B3E1D6E72A0ADA2800F2905A /* Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E62A0ADA2800F2905A /* Delegates.swift */; }; B3E1D6E82A0ADCF900F2905A /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B329909A296A1F7F00D246E9 /* layout.txt */; }; + B3E8A5162A4CF67700302473 /* Circular.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E8A5152A4CF67700302473 /* Circular.swift */; }; + B3E8A5172A4CF67700302473 /* Circular.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E8A5152A4CF67700302473 /* Circular.swift */; }; + B3E8A5192A4CF6BD00302473 /* CountDown.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E8A5182A4CF6BD00302473 /* CountDown.swift */; }; + B3E8A51A2A4CF6BD00302473 /* CountDown.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E8A5182A4CF6BD00302473 /* CountDown.swift */; }; + B3E8A51C2A4CF77000302473 /* Corner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E8A51B2A4CF77000302473 /* Corner.swift */; }; + B3F85EAD2A4A5A0B00F8B40B /* HoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */; }; + B3F85EAE2A4A5A0B00F8B40B /* HoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */; }; + B3F85EAF2A4A5A0B00F8B40B /* HoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */; }; + B3F85EB22A4A5F6900F8B40B /* DateTimeAdjust.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EB12A4A5F6900F8B40B /* DateTimeAdjust.swift */; }; + B3F85EB42A4A5FA000F8B40B /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EB32A4A5FA000F8B40B /* Setting.swift */; }; D245D60926FA886200A89044 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; - D2633073270CF85F0053B9F6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2633071270CF85F0053B9F6 /* Main.storyboard */; }; D26CF1C026FD0C8D004EE9BB /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26CF1BF26FD0C8D004EE9BB /* Layout.swift */; }; D2CFF74D270FF940000CECDA /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; - D2E4E0E626F7C73E002F3716 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E526F7C73E002F3716 /* AppDelegate.swift */; }; - D2E4E0EB26F7C73E002F3716 /* ChineseTime.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */; }; + D2E4E0E626F7C73E002F3716 /* macApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E526F7C73E002F3716 /* macApp.swift */; }; D2E4E0ED26F7C73F002F3716 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2E4E0EC26F7C73F002F3716 /* Assets.xcassets */; }; - D2E4E0F926F7C908002F3716 /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0F826F7C908002F3716 /* WatchFace.swift */; }; D2F0825D26FAB23500ADBE13 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; /* End PBXBuildFile section */ @@ -203,7 +237,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - B3CC8BA72A0B30BC0063DE44 /* iOS Widget Extension.appex in Embed Foundation Extensions */, + B3CC8BA72A0B30BC0063DE44 /* Chinese Time Widget.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -214,7 +248,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - B3CC8BFC2A0C7E310063DE44 /* Watch Widget Extension.appex in Embed Foundation Extensions */, + B3CC8BFC2A0C7E310063DE44 /* Chinese Time Widget.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -225,7 +259,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - B3E1D6D82A0AB43E00F2905A /* Mac Widget Extension.appex in Embed Foundation Extensions */, + B3E1D6D82A0AB43E00F2905A /* Chinese Time Widget.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -233,124 +267,89 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 9E0438C72A8FD5D7007217A8 /* Locale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locale.swift; sourceTree = ""; }; + 9E57427E2AA501E70052AE70 /* TaskGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskGroup.swift; sourceTree = ""; }; + 9E71FD062A50BF2E00C9CA78 /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatchFace.swift; path = macOS/Views/WatchFace.swift; sourceTree = SOURCE_ROOT; }; + 9E820BE62A7DEA1700453389 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + 9E90D7EE2A9EABD100855F2C /* Environments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environments.swift; sourceTree = ""; }; + 9E9889EE2A79EABF0066414A /* watchPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchPanel.swift; sourceTree = ""; }; + 9E9FB80D2A9BE02200888FA3 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 9EBFBE322A58A40900DC42AF /* ThemeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ThemeData.swift; path = Shared/DataModel/ThemeData.swift; sourceTree = SOURCE_ROOT; }; B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceHanSansKR-Heavy.otf"; sourceTree = ""; }; - B301073F2A099A0700D0A50C /* Chinese-Time-Watch-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Chinese-Time-Watch-Info.plist"; sourceTree = ""; }; - B32243E12A0D3BF600E7AED5 /* WatchLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchLayout.swift; sourceTree = ""; }; - B32243E42A0D8B6C00E7AED5 /* WatchWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchWidgetView.swift; sourceTree = ""; }; + B301073F2A099A0700D0A50C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B32243E12A0D3BF600E7AED5 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; + B32243E42A0D8B6C00E7AED5 /* WatchWidgetBasic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchWidgetBasic.swift; sourceTree = ""; }; B32243E72A0D8E5000E7AED5 /* WatchWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchWidgetBundle.swift; sourceTree = ""; }; B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIUtilities.swift; sourceTree = ""; }; - B32243F42A0DD17F00E7AED5 /* WatchWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WatchWidgetExtension.entitlements; sourceTree = ""; }; + B32243F42A0DD17F00E7AED5 /* WatchWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WatchWidget.entitlements; sourceTree = ""; }; + B32833392A46685200E36989 /* LayoutSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutSetting.swift; sourceTree = ""; }; + B328333B2A46687D00E36989 /* ColorSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSetting.swift; sourceTree = ""; }; + B328333D2A4668B000E36989 /* RingSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingSetting.swift; sourceTree = ""; }; + B328333F2A4668EA00E36989 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + B32833412A46691800E36989 /* Documentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Documentation.swift; sourceTree = ""; }; + B32833432A46695000E36989 /* Datetime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datetime.swift; sourceTree = ""; }; + B32833452A4739FD00E36989 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; + B328A2EE2A3D19A4002191F4 /* ThemesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemesList.swift; sourceTree = ""; }; B329909A296A1F7F00D246E9 /* layout.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = layout.txt; sourceTree = ""; }; + B32999212A4F96D600B71579 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; + B32999282A4F9B7B00B71579 /* LocationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; B34DA20829FDC0B200562449 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Utilities.swift; path = Shared/Utilities.swift; sourceTree = SOURCE_ROOT; }; - B34DA20B29FDFE3800562449 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - B35097E02A0AEC9F001AB3CE /* WidgetFaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetFaceView.swift; sourceTree = ""; }; - B3515CAB29F6147100E6BCDC /* Chinese Time iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chinese Time iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - B3515CE029F6149400E6BCDC /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - B3515CE129F6149400E6BCDC /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - B3515CE329F6149500E6BCDC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B3515CAB29F6147100E6BCDC /* Chinese Time.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chinese Time.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + B3515CE329F6149500E6BCDC /* iOSApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; B3515CE529F6149500E6BCDC /* Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; B3515CE629F6149500E6BCDC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B3515CE829F6149500E6BCDC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - B3515CEC29F6149500E6BCDC /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; B3515CFC29F6153E00E6BCDC /* layout.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = layout.txt; sourceTree = ""; }; - B3515D0629F6189F00E6BCDC /* ChineseTime.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChineseTime.entitlements; sourceTree = ""; }; + B3515D0629F6189F00E6BCDC /* ChineseTimeMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChineseTimeMac.entitlements; sourceTree = ""; }; B36D2F782A047A2000005162 /* WatchFaceBasics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFaceBasics.swift; sourceTree = ""; }; B37063A229FAFF3300CC6E57 /* MetaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaLayout.swift; sourceTree = ""; }; - B37063A529FB088900CC6E57 /* MetaWatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaWatchFace.swift; sourceTree = ""; }; - B37063A829FB0C4600CC6E57 /* WatchFace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; sourceTree = ""; }; - B379AD6D29FF0C03009C373E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + B383A6CD2A4D02D8002FADCF /* Single.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Single.swift; sourceTree = ""; }; + B383A6D02A4D02E2002FADCF /* Dual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dual.swift; sourceTree = ""; }; + B383A6D32A4D1C7B002FADCF /* Relevance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relevance.swift; sourceTree = ""; }; + B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + B38CC0482A4F1F1600F4DB9F /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; sourceTree = ""; }; B39085FE2A0314DD00943F2B /* Chinese Time.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chinese Time.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - B39086002A0314DD00943F2B /* ChineseTimeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChineseTimeApp.swift; sourceTree = ""; }; + B39086002A0314DD00943F2B /* watchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = watchApp.swift; sourceTree = ""; }; B39086022A0314DD00943F2B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; B39086042A0314DD00943F2B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B39086112A0317CB00943F2B /* RoundedRect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedRect.swift; sourceTree = ""; }; B39086192A03522800943F2B /* layout.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = layout.txt; sourceTree = ""; }; B395B5A02A0ED7EF003206E7 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; - B395B5AD2A0F3A06003206E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/WatchWidget.intentdefinition; sourceTree = ""; }; - B395B5B02A0F3A38003206E7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/WatchWidget.strings"; sourceTree = ""; }; - B395B5B22A0F3A39003206E7 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/WatchWidget.strings"; sourceTree = ""; }; - B395B5B42A0F3A3A003206E7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WatchWidget.strings; sourceTree = ""; }; - B3AF0B8D29FB658600C78081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - B3AF0B9029FB658600C78081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3AF0B9329FB658600C78081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - B3AF0B9629FB658600C78081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3AF0B9829FB6C6400C78081 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3AF0B9929FB6C6400C78081 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3AF0B9A29FB6C6400C78081 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3AF0B9B29FB6C6400C78081 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3AF0B9C29FB6C6D00C78081 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3AF0B9D29FB6C6D00C78081 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3AF0B9E29FB6C6D00C78081 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; 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 = ""; }; - B3BFA25D2A06BBA60018F99E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = "en.lproj/ChineseTime Watch App-InfoPlist.strings"; sourceTree = ""; }; - B3BFA2602A06BBA60018F99E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3BFA2622A06BBAF0018F99E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ChineseTime Watch App-InfoPlist.strings"; sourceTree = ""; }; - B3BFA2632A06BBAF0018F99E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3BFA2642A06BBB50018F99E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/ChineseTime Watch App-InfoPlist.strings"; sourceTree = ""; }; - B3CC8B972A0B30BB0063DE44 /* iOS Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "iOS Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + B3CC8B972A0B30BB0063DE44 /* Chinese Time Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Chinese Time 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 = ""; }; B3CC8BA22A0B30BC0063DE44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B3CC8BB62A0B330C0063DE44 /* Widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Widget.swift; sourceTree = ""; }; - B3CC8BC22A0BE4680063DE44 /* Chinese Time iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Chinese Time iOS.entitlements"; sourceTree = ""; }; - B3CC8BC32A0BE47D0063DE44 /* iOSWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOSWidgetExtension.entitlements; sourceTree = ""; }; - B3CC8BC42A0BE48C0063DE44 /* Chinese Time Watch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Chinese Time Watch.entitlements"; sourceTree = ""; }; - B3CC8BC62A0C7A090063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - B3CC8BC92A0C7A090063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3CC8BCC2A0C7A090063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - B3CC8BCF2A0C7A090063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = "en.lproj/Chinese-Time-Watch-InfoPlist.strings"; sourceTree = ""; }; - B3CC8BD12A0C7A1F0063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3CC8BD22A0C7A1F0063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3CC8BD32A0C7A1F0063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3CC8BD52A0C7A1F0063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3CC8BD72A0C7A1F0063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Chinese-Time-Watch-InfoPlist.strings"; sourceTree = ""; }; - B3CC8BD82A0C7A2A0063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3CC8BD92A0C7A2A0063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; - B3CC8BDA2A0C7A2A0063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3CC8BDB2A0C7A2A0063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; - B3CC8BDC2A0C7A2A0063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Chinese-Time-Watch-InfoPlist.strings"; sourceTree = ""; }; - B3CC8BE12A0C7A3C0063DE44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Widget.intentdefinition; sourceTree = ""; }; - B3CC8BE42A0C7A400063DE44 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Widget.strings"; sourceTree = ""; }; - B3CC8BE62A0C7A410063DE44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Widget.strings"; sourceTree = ""; }; - B3CC8BE82A0C7A410063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Widget.strings; sourceTree = ""; }; - B3CC8BE92A0C7CEB0063DE44 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3CC8BEE2A0C7E300063DE44 /* Watch Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Watch Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - B3CC8BF22A0C7E300063DE44 /* WatchWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchWidget.swift; sourceTree = ""; }; + B3CC8BB62A0B330C0063DE44 /* Full.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Full.swift; sourceTree = ""; }; + B3CC8BC22A0BE4680063DE44 /* ChineseTimeiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChineseTimeiOS.entitlements; sourceTree = ""; }; + B3CC8BC32A0BE47D0063DE44 /* iOSWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOSWidget.entitlements; sourceTree = ""; }; + B3CC8BC42A0BE48C0063DE44 /* ChineseTimeWatch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChineseTimeWatch.entitlements; sourceTree = ""; }; + B3CC8BEE2A0C7E300063DE44 /* Chinese Time Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Chinese Time Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + B3CC8BF22A0C7E300063DE44 /* TextDesp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDesp.swift; sourceTree = ""; }; B3CC8BF52A0C7E310063DE44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B3CC8BF72A0C7E310063DE44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B3D239432A0F461C00E506EB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - B3D239462A0F461C00E506EB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - B3D239482A0F462600E506EB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3D239492A0F462600E506EB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - B3D2394A2A0F463000E506EB /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; - B3D2394B2A0F463000E506EB /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; - B3D2394C2A0F463000E506EB /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; - B3E1D6C62A0AB43E00F2905A /* Mac Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Mac Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - B3E1D6C82A0AB43E00F2905A /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; - B3E1D6CA2A0AB43E00F2905A /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + B3E1D6C62A0AB43E00F2905A /* Chinese Time Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Chinese Time Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; B3E1D6CD2A0AB43E00F2905A /* MacWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacWidgetBundle.swift; sourceTree = ""; }; B3E1D6D02A0AB43E00F2905A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B3E1D6D22A0AB43E00F2905A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B3E1D6D32A0AB43E00F2905A /* MacWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MacWidget.entitlements; sourceTree = ""; }; B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFaceView.swift; sourceTree = ""; }; - B3E1D6E62A0ADA2800F2905A /* Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Delegates.swift; path = Shared/Delegates.swift; sourceTree = SOURCE_ROOT; }; - B3FE725729FB5E0C009554C2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; - B3FE725829FB5E0E009554C2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; - B3FE725929FB5E14009554C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - B3FE725A29FB5E31009554C2 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; - B3FE725B29FB5E3A009554C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - B3FE725C29FB5E42009554C2 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; - B3FE725D29FB5E45009554C2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; - B3FE725E29FB5E47009554C2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; + B3E8A5152A4CF67700302473 /* Circular.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Circular.swift; sourceTree = ""; }; + B3E8A5182A4CF6BD00302473 /* CountDown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountDown.swift; sourceTree = ""; }; + B3E8A51B2A4CF77000302473 /* Corner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Corner.swift; sourceTree = ""; }; + B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverView.swift; sourceTree = ""; }; + B3F85EB12A4A5F6900F8B40B /* DateTimeAdjust.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeAdjust.swift; sourceTree = ""; }; + B3F85EB32A4A5FA000F8B40B /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; D245D60826FA886200A89044 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; D26CF1BF26FD0C8D004EE9BB /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; D2CFF74C270FF940000CECDA /* PlanetModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanetModel.swift; sourceTree = ""; }; D2E4E0E226F7C73E002F3716 /* Chinese Time.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chinese Time.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - D2E4E0E526F7C73E002F3716 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D2E4E0EA26F7C73E002F3716 /* ChineseTime.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ChineseTime.xcdatamodel; sourceTree = ""; }; + D2E4E0E526F7C73E002F3716 /* macApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = macApp.swift; sourceTree = ""; }; D2E4E0EC26F7C73F002F3716 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D2E4E0F126F7C73F002F3716 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D2E4E0F826F7C908002F3716 /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; sourceTree = ""; }; D2F0825C26FAB23500ADBE13 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -373,8 +372,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B3CC8B992A0B30BB0063DE44 /* SwiftUI.framework in Frameworks */, - B3CC8B982A0B30BB0063DE44 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -382,8 +379,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B3CC8BF02A0C7E300063DE44 /* SwiftUI.framework in Frameworks */, - B3CC8BEF2A0C7E300063DE44 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -391,8 +386,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B3E1D6CB2A0AB43E00F2905A /* SwiftUI.framework in Frameworks */, - B3E1D6C92A0AB43E00F2905A /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -406,22 +399,75 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9E910DDF2A525BCF009A1C54 /* Setting */ = { + isa = PBXGroup; + children = ( + B328A2EE2A3D19A4002191F4 /* ThemesList.swift */, + B328333B2A46687D00E36989 /* ColorSetting.swift */, + B328333D2A4668B000E36989 /* RingSetting.swift */, + B32833412A46691800E36989 /* Documentation.swift */, + B32833392A46685200E36989 /* LayoutSetting.swift */, + B32833432A46695000E36989 /* Datetime.swift */, + B32833452A4739FD00E36989 /* Location.swift */, + ); + path = Setting; + sourceTree = ""; + }; + B32833382A46681E00E36989 /* Views */ = { + isa = PBXGroup; + children = ( + B3BCCEE72A48746000F5745E /* Setting.swift */, + B3BEB4C22A48994C000751D5 /* WatchFace.swift */, + B328333F2A4668EA00E36989 /* Welcome.swift */, + ); + path = Views; + sourceTree = ""; + }; + B32999272A4F9B5200B71579 /* DataModel */ = { + isa = PBXGroup; + children = ( + 9EBFBE322A58A40900DC42AF /* ThemeData.swift */, + D245D60826FA886200A89044 /* Model.swift */, + D2CFF74C270FF940000CECDA /* PlanetModel.swift */, + D2F0825C26FAB23500ADBE13 /* Data.swift */, + B32999282A4F9B7B00B71579 /* LocationManager.swift */, + B37063A229FAFF3300CC6E57 /* MetaLayout.swift */, + ); + path = DataModel; + sourceTree = ""; + }; + B329992A2A4F9BB500B71579 /* Views */ = { + isa = PBXGroup; + children = ( + B36D2F782A047A2000005162 /* WatchFaceBasics.swift */, + B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */, + B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */, + B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */, + B39086112A0317CB00943F2B /* RoundedRect.swift */, + ); + path = Views; + sourceTree = ""; + }; + B32999332A4F9E1700B71579 /* Views */ = { + isa = PBXGroup; + children = ( + 9E71FD062A50BF2E00C9CA78 /* WatchFace.swift */, + B32999212A4F96D600B71579 /* Setting.swift */, + 9E820BE62A7DEA1700453389 /* Welcome.swift */, + ); + path = Views; + sourceTree = ""; + }; B3515CAC29F6147100E6BCDC /* iOS */ = { isa = PBXGroup; children = ( - B3515CE329F6149500E6BCDC /* AppDelegate.swift */, - B3515CEC29F6149500E6BCDC /* SceneDelegate.swift */, + B32833382A46681E00E36989 /* Views */, + B3515CE329F6149500E6BCDC /* iOSApp.swift */, B3515CE529F6149500E6BCDC /* Layout.swift */, - B3515CE029F6149400E6BCDC /* ViewController.swift */, - B37063A829FB0C4600CC6E57 /* WatchFace.swift */, - B33CC9BE29FB583B00426C92 /* Main.storyboard */, - B3515CE129F6149400E6BCDC /* LaunchScreen.storyboard */, B3515CE629F6149500E6BCDC /* Info.plist */, B3515CFC29F6153E00E6BCDC /* layout.txt */, - B3CC8BC22A0BE4680063DE44 /* Chinese Time iOS.entitlements */, + B3CC8BC22A0BE4680063DE44 /* ChineseTimeiOS.entitlements */, B3515CE829F6149500E6BCDC /* Assets.xcassets */, - B3AF0B8F29FB658600C78081 /* Localizable.strings */, - B3AF0B8C29FB658600C78081 /* InfoPlist.strings */, ); path = iOS; sourceTree = ""; @@ -430,16 +476,15 @@ isa = PBXGroup; children = ( B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */, - D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */, B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */, - D245D60826FA886200A89044 /* Model.swift */, - D2CFF74C270FF940000CECDA /* PlanetModel.swift */, - D2F0825C26FAB23500ADBE13 /* Data.swift */, - B3E1D6E62A0ADA2800F2905A /* Delegates.swift */, - B37063A229FAFF3300CC6E57 /* MetaLayout.swift */, - B37063A529FB088900CC6E57 /* MetaWatchFace.swift */, - B39086112A0317CB00943F2B /* RoundedRect.swift */, B34DA20829FDC0B200562449 /* Utilities.swift */, + 9E0438C72A8FD5D7007217A8 /* Locale.swift */, + 9E90D7EE2A9EABD100855F2C /* Environments.swift */, + B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */, + B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */, + B32999272A4F9B5200B71579 /* DataModel */, + B329992A2A4F9BB500B71579 /* Views */, + 9E910DDF2A525BCF009A1C54 /* Setting */, ); path = Shared; sourceTree = ""; @@ -447,19 +492,13 @@ B39085FF2A0314DD00943F2B /* Watch */ = { isa = PBXGroup; children = ( - B39086002A0314DD00943F2B /* ChineseTimeApp.swift */, - B36D2F782A047A2000005162 /* WatchFaceBasics.swift */, - B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */, - B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */, - B32243E12A0D3BF600E7AED5 /* WatchLayout.swift */, - B39086022A0314DD00943F2B /* ContentView.swift */, - B301073F2A099A0700D0A50C /* Chinese-Time-Watch-Info.plist */, - B3CC8BCE2A0C7A090063DE44 /* Chinese-Time-Watch-InfoPlist.strings */, + B3F85EB02A4A5F3B00F8B40B /* Views */, + B39086002A0314DD00943F2B /* watchApp.swift */, + B32243E12A0D3BF600E7AED5 /* Layout.swift */, + B301073F2A099A0700D0A50C /* Info.plist */, B39086192A03522800943F2B /* layout.txt */, - B3CC8BC42A0BE48C0063DE44 /* Chinese Time Watch.entitlements */, + B3CC8BC42A0BE48C0063DE44 /* ChineseTimeWatch.entitlements */, B39086042A0314DD00943F2B /* Assets.xcassets */, - B3BFA25F2A06BBA60018F99E /* Localizable.strings */, - B3BFA25C2A06BBA60018F99E /* ChineseTime Watch App-InfoPlist.strings */, ); path = Watch; sourceTree = ""; @@ -468,11 +507,9 @@ isa = PBXGroup; children = ( B3CC8B9B2A0B30BB0063DE44 /* iOSWidgetBundle.swift */, - B3CC8BC32A0BE47D0063DE44 /* iOSWidgetExtension.entitlements */, - B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */, B3CC8BA22A0B30BC0063DE44 /* Info.plist */, - B3CC8BC82A0C7A090063DE44 /* Localizable.strings */, - B3CC8BC52A0C7A090063DE44 /* InfoPlist.strings */, + B3CC8BC32A0BE47D0063DE44 /* iOSWidget.entitlements */, + B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */, ); path = iOSWidget; sourceTree = ""; @@ -480,9 +517,12 @@ B3CC8BBE2A0B42990063DE44 /* Widget */ = { isa = PBXGroup; children = ( - B3CC8BB62A0B330C0063DE44 /* Widget.swift */, - B35097E02A0AEC9F001AB3CE /* WidgetFaceView.swift */, - B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */, + B383A6CD2A4D02D8002FADCF /* Single.swift */, + B383A6D02A4D02E2002FADCF /* Dual.swift */, + B3CC8BB62A0B330C0063DE44 /* Full.swift */, + 9E57427E2AA501E70052AE70 /* TaskGroup.swift */, + B3E8A5142A4CF64A00302473 /* WatchWidgets */, + 9E9FB80D2A9BE02200888FA3 /* Localizable.xcstrings */, ); path = Widget; sourceTree = ""; @@ -491,41 +531,49 @@ isa = PBXGroup; children = ( B32243E72A0D8E5000E7AED5 /* WatchWidgetBundle.swift */, - B3CC8BF22A0C7E300063DE44 /* WatchWidget.swift */, - B32243E42A0D8B6C00E7AED5 /* WatchWidgetView.swift */, - B395B5A02A0ED7EF003206E7 /* IconView.swift */, - B32243F42A0DD17F00E7AED5 /* WatchWidgetExtension.entitlements */, - B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */, - B3CC8BF52A0C7E310063DE44 /* Assets.xcassets */, + B32243F42A0DD17F00E7AED5 /* WatchWidget.entitlements */, B3CC8BF72A0C7E310063DE44 /* Info.plist */, - B3D239452A0F461C00E506EB /* Localizable.strings */, - B3D239422A0F461C00E506EB /* InfoPlist.strings */, + B3CC8BF52A0C7E310063DE44 /* Assets.xcassets */, ); path = WatchWidget; sourceTree = ""; }; - B3E1D6C72A0AB43E00F2905A /* Frameworks */ = { - isa = PBXGroup; - children = ( - B3E1D6C82A0AB43E00F2905A /* WidgetKit.framework */, - B3E1D6CA2A0AB43E00F2905A /* SwiftUI.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; B3E1D6CC2A0AB43E00F2905A /* MacWidget */ = { isa = PBXGroup; children = ( B3E1D6CD2A0AB43E00F2905A /* MacWidgetBundle.swift */, - B3E1D6D02A0AB43E00F2905A /* Assets.xcassets */, B3E1D6D22A0AB43E00F2905A /* Info.plist */, - B3CC8BD42A0C7A1F0063DE44 /* Localizable.strings */, - B3CC8BCB2A0C7A090063DE44 /* InfoPlist.strings */, B3E1D6D32A0AB43E00F2905A /* MacWidget.entitlements */, + B3E1D6D02A0AB43E00F2905A /* Assets.xcassets */, ); path = MacWidget; sourceTree = ""; }; + B3E8A5142A4CF64A00302473 /* WatchWidgets */ = { + isa = PBXGroup; + children = ( + B383A6D32A4D1C7B002FADCF /* Relevance.swift */, + B3E8A5152A4CF67700302473 /* Circular.swift */, + B3E8A5182A4CF6BD00302473 /* CountDown.swift */, + B3CC8BF22A0C7E300063DE44 /* TextDesp.swift */, + B3E8A51B2A4CF77000302473 /* Corner.swift */, + B32243E42A0D8B6C00E7AED5 /* WatchWidgetBasic.swift */, + B395B5A02A0ED7EF003206E7 /* IconView.swift */, + ); + path = WatchWidgets; + sourceTree = ""; + }; + B3F85EB02A4A5F3B00F8B40B /* Views */ = { + isa = PBXGroup; + children = ( + B3F85EB12A4A5F6900F8B40B /* DateTimeAdjust.swift */, + B3F85EB32A4A5FA000F8B40B /* Setting.swift */, + B38CC0482A4F1F1600F4DB9F /* WatchFace.swift */, + B39086022A0314DD00943F2B /* ContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; D2E4E0D926F7C73E002F3716 = { isa = PBXGroup; children = ( @@ -537,7 +585,6 @@ B3E1D6CC2A0AB43E00F2905A /* MacWidget */, B3CC8B9A2A0B30BB0063DE44 /* iOSWidget */, B3CC8BF12A0C7E300063DE44 /* WatchWidget */, - B3E1D6C72A0AB43E00F2905A /* Frameworks */, D2E4E0E326F7C73E002F3716 /* Products */, ); sourceTree = ""; @@ -546,11 +593,11 @@ isa = PBXGroup; children = ( D2E4E0E226F7C73E002F3716 /* Chinese Time.app */, - B3515CAB29F6147100E6BCDC /* Chinese Time iOS.app */, + B3515CAB29F6147100E6BCDC /* Chinese Time.app */, B39085FE2A0314DD00943F2B /* Chinese Time.app */, - B3E1D6C62A0AB43E00F2905A /* Mac Widget Extension.appex */, - B3CC8B972A0B30BB0063DE44 /* iOS Widget Extension.appex */, - B3CC8BEE2A0C7E300063DE44 /* Watch Widget Extension.appex */, + B3E1D6C62A0AB43E00F2905A /* Chinese Time Widget.appex */, + B3CC8B972A0B30BB0063DE44 /* Chinese Time Widget.appex */, + B3CC8BEE2A0C7E300063DE44 /* Chinese Time Widget.appex */, ); name = Products; sourceTree = ""; @@ -558,17 +605,14 @@ D2E4E0E426F7C73E002F3716 /* macOS */ = { isa = PBXGroup; children = ( - D2E4E0E526F7C73E002F3716 /* AppDelegate.swift */, + B32999332A4F9E1700B71579 /* Views */, + D2E4E0E526F7C73E002F3716 /* macApp.swift */, + 9E9889EE2A79EABF0066414A /* watchPanel.swift */, D26CF1BF26FD0C8D004EE9BB /* Layout.swift */, - B34DA20B29FDFE3800562449 /* ViewController.swift */, - D2E4E0F826F7C908002F3716 /* WatchFace.swift */, - D2633071270CF85F0053B9F6 /* Main.storyboard */, B329909A296A1F7F00D246E9 /* layout.txt */, D2E4E0F126F7C73F002F3716 /* Info.plist */, - B3515D0629F6189F00E6BCDC /* ChineseTime.entitlements */, + B3515D0629F6189F00E6BCDC /* ChineseTimeMac.entitlements */, D2E4E0EC26F7C73F002F3716 /* Assets.xcassets */, - B3AF0B9529FB658600C78081 /* Localizable.strings */, - B3AF0B9229FB658600C78081 /* InfoPlist.strings */, ); path = macOS; sourceTree = ""; @@ -594,7 +638,7 @@ ); name = "Chinese Time iOS"; productName = "Chinese Time iOS"; - productReference = B3515CAB29F6147100E6BCDC /* Chinese Time iOS.app */; + productReference = B3515CAB29F6147100E6BCDC /* Chinese Time.app */; productType = "com.apple.product-type.application"; }; B39085FD2A0314DD00943F2B /* Chinese Time Watch */ = { @@ -630,7 +674,7 @@ ); name = "iOS Widget Extension"; productName = iOSWidgetExtension; - productReference = B3CC8B972A0B30BB0063DE44 /* iOS Widget Extension.appex */; + productReference = B3CC8B972A0B30BB0063DE44 /* Chinese Time Widget.appex */; productType = "com.apple.product-type.app-extension"; }; B3CC8BED2A0C7E300063DE44 /* Watch Widget Extension */ = { @@ -647,7 +691,7 @@ ); name = "Watch Widget Extension"; productName = WatchWidgetExtension; - productReference = B3CC8BEE2A0C7E300063DE44 /* Watch Widget Extension.appex */; + productReference = B3CC8BEE2A0C7E300063DE44 /* Chinese Time Widget.appex */; productType = "com.apple.product-type.app-extension"; }; B3E1D6C52A0AB43E00F2905A /* Mac Widget Extension */ = { @@ -664,12 +708,12 @@ ); name = "Mac Widget Extension"; productName = MacWidgetExtension; - productReference = B3E1D6C62A0AB43E00F2905A /* Mac Widget Extension.appex */; + productReference = B3E1D6C62A0AB43E00F2905A /* Chinese Time Widget.appex */; productType = "com.apple.product-type.app-extension"; }; - D2E4E0E126F7C73E002F3716 /* Chinese Time */ = { + D2E4E0E126F7C73E002F3716 /* Chinese Time Mac */ = { isa = PBXNativeTarget; - buildConfigurationList = D2E4E0F526F7C73F002F3716 /* Build configuration list for PBXNativeTarget "Chinese Time" */; + buildConfigurationList = D2E4E0F526F7C73F002F3716 /* Build configuration list for PBXNativeTarget "Chinese Time Mac" */; buildPhases = ( D2E4E0DE26F7C73E002F3716 /* Sources */, D2E4E0DF26F7C73E002F3716 /* Frameworks */, @@ -682,7 +726,7 @@ dependencies = ( B3E1D6D72A0AB43E00F2905A /* PBXTargetDependency */, ); - name = "Chinese Time"; + name = "Chinese Time Mac"; packageProductDependencies = ( ); productName = ChineseTime; @@ -697,7 +741,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1500; TargetAttributes = { B3515CAA29F6147100E6BCDC = { CreatedOnToolsVersion = 14.3; @@ -721,12 +765,11 @@ }; }; buildConfigurationList = D2E4E0DD26F7C73E002F3716 /* Build configuration list for PBXProject "Chinese Time" */; - compatibilityVersion = "Xcode 13.0"; + compatibilityVersion = "Xcode 15.0"; developmentRegion = "zh-Hant"; hasScannedForEncodings = 0; knownRegions = ( en, - Base, "zh-Hant", "zh-Hans", ); @@ -737,7 +780,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - D2E4E0E126F7C73E002F3716 /* Chinese Time */, + D2E4E0E126F7C73E002F3716 /* Chinese Time Mac */, B3515CAA29F6147100E6BCDC /* Chinese Time iOS */, B39085FD2A0314DD00943F2B /* Chinese Time Watch */, B3E1D6C52A0AB43E00F2905A /* Mac Widget Extension */, @@ -752,13 +795,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B383A6D82A4D1EA1002FADCF /* InfoPlist.xcstrings in Resources */, B3515CFD29F6153E00E6BCDC /* layout.txt in Resources */, - B3AF0B9129FB658600C78081 /* Localizable.strings in Resources */, - B3515CEF29F6149500E6BCDC /* LaunchScreen.storyboard in Resources */, - B33CC9BC29FB583B00426C92 /* Main.storyboard in Resources */, + 9E0C5BBA2A9BCFDC00503CE0 /* Localizable.xcstrings in Resources */, B3515CF629F6149500E6BCDC /* Assets.xcassets in Resources */, B301073D2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf in Resources */, - B3AF0B8E29FB658600C78081 /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -766,10 +807,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3BFA2612A06BBA60018F99E /* Localizable.strings in Resources */, + 9EE0B02B2A9BD5F700FC97D5 /* InfoPlist.xcstrings in Resources */, B390861A2A03522800943F2B /* layout.txt in Resources */, - B3CC8BD02A0C7A090063DE44 /* Chinese-Time-Watch-InfoPlist.strings in Resources */, - B3BFA25E2A06BBA60018F99E /* ChineseTime Watch App-InfoPlist.strings in Resources */, + 9E0C5BBB2A9BCFDD00503CE0 /* Localizable.xcstrings in Resources */, B301073E2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf in Resources */, B39086052A0314DD00943F2B /* Assets.xcassets in Resources */, ); @@ -779,9 +819,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3CC8BCA2A0C7A090063DE44 /* Localizable.strings in Resources */, + 9EE0B02D2A9BD5F800FC97D5 /* InfoPlist.xcstrings in Resources */, B3CC8BB82A0B333F0063DE44 /* layout.txt in Resources */, - B3CC8BC72A0C7A090063DE44 /* InfoPlist.strings in Resources */, + 9E21B0352A9BE14B00E59F99 /* Localizable.xcstrings in Resources */, B3CC8BB92A0B334E0063DE44 /* SourceHanSansKR-Heavy.otf in Resources */, B3CC8BA12A0B30BC0063DE44 /* Assets.xcassets in Resources */, ); @@ -791,9 +831,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9EE0B02E2A9BD5F800FC97D5 /* InfoPlist.xcstrings in Resources */, B3CE494F2A111971007905C1 /* SourceHanSansKR-Heavy.otf in Resources */, - B3D239472A0F461C00E506EB /* Localizable.strings in Resources */, - B3D239442A0F461C00E506EB /* InfoPlist.strings in Resources */, + 9E21B0362A9BE14B00E59F99 /* Localizable.xcstrings in Resources */, B32243EC2A0D917800E7AED5 /* layout.txt in Resources */, B3CC8BF62A0C7E310063DE44 /* Assets.xcassets in Resources */, ); @@ -803,9 +843,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3CC8BD62A0C7A1F0063DE44 /* Localizable.strings in Resources */, + 9EE0B02C2A9BD5F700FC97D5 /* InfoPlist.xcstrings in Resources */, B3E1D6E82A0ADCF900F2905A /* layout.txt in Resources */, - B3CC8BCD2A0C7A090063DE44 /* InfoPlist.strings in Resources */, + 9E21B0342A9BE14A00E59F99 /* Localizable.xcstrings in Resources */, B35097DF2A0AEAF3001AB3CE /* SourceHanSansKR-Heavy.otf in Resources */, B3E1D6D12A0AB43E00F2905A /* Assets.xcassets in Resources */, ); @@ -815,10 +855,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3AF0B9729FB658600C78081 /* Localizable.strings in Resources */, + 9EE0B02A2A9BD5F600FC97D5 /* InfoPlist.xcstrings in Resources */, + B383A6ED2A4D1EA2002FADCF /* Localizable.xcstrings in Resources */, B329909B296A1F7F00D246E9 /* layout.txt in Resources */, - B3AF0B9429FB658600C78081 /* InfoPlist.strings in Resources */, - D2633073270CF85F0053B9F6 /* Main.storyboard in Resources */, B30107402A09A0A500D0A50C /* SourceHanSansKR-Heavy.otf in Resources */, D2E4E0ED26F7C73F002F3716 /* Assets.xcassets in Resources */, ); @@ -832,22 +871,34 @@ buildActionMask = 2147483647; files = ( B3515D0129F616A200E6BCDC /* Model.swift in Sources */, - B3CC8BDE2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */, + B328A2EF2A3D19A4002191F4 /* ThemesList.swift in Sources */, B3515CFF29F6169D00E6BCDC /* Data.swift in Sources */, - B37063A729FB088900CC6E57 /* MetaWatchFace.swift in Sources */, + B3BCCEE82A48746000F5745E /* Setting.swift in Sources */, + B3BEB4C32A48994C000751D5 /* WatchFace.swift in Sources */, + 9EBFBE342A58A40900DC42AF /* ThemeData.swift in Sources */, B3515D0029F616A000E6BCDC /* PlanetModel.swift in Sources */, + 9E0438C92A8FD5D7007217A8 /* Locale.swift in Sources */, + B32833442A46695000E36989 /* Datetime.swift in Sources */, + B32833462A4739FD00E36989 /* Location.swift in Sources */, + B328333E2A4668B000E36989 /* RingSetting.swift in Sources */, B3BFA2572A05E0590018F99E /* WatchConnectivity.swift in Sources */, - B3515CF129F6149500E6BCDC /* AppDelegate.swift in Sources */, - B3515CFA29F6149500E6BCDC /* SceneDelegate.swift in Sources */, - B3CC8BC02A0BCB5F0063DE44 /* Delegates.swift in Sources */, - B3515CEE29F6149500E6BCDC /* ViewController.swift in Sources */, + B329992E2A4F9C8500B71579 /* LocationManager.swift in Sources */, + B3F85EAE2A4A5A0B00F8B40B /* HoverView.swift in Sources */, + 9E6E10CD2AA6410A004CEDBE /* TaskGroup.swift in Sources */, + B3515CF129F6149500E6BCDC /* iOSApp.swift in Sources */, + B328333C2A46687D00E36989 /* ColorSetting.swift in Sources */, + 9E90D7F02A9EABD100855F2C /* Environments.swift in Sources */, + B32833402A4668EA00E36989 /* Welcome.swift in Sources */, + 9E5A41222AA61FC400B470BE /* Relevance.swift in Sources */, B37063A429FAFF3300CC6E57 /* MetaLayout.swift in Sources */, B3515CF329F6149500E6BCDC /* Layout.swift in Sources */, - B37063A929FB0C4600CC6E57 /* WatchFace.swift in Sources */, - B3D318DD2A1661C1009FC18D /* ChineseTime.xcdatamodeld in Sources */, + B3BEB4C52A489A10000751D5 /* WatchFaceView.swift in Sources */, + B328333A2A46685200E36989 /* LayoutSetting.swift in Sources */, + B32833422A46691800E36989 /* Documentation.swift in Sources */, B39086132A0317CB00943F2B /* RoundedRect.swift in Sources */, - B395B5A92A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */, + B3BEB4C62A489A99000751D5 /* SwiftUIUtilities.swift in Sources */, B34DA20A29FDC0B200562449 /* Utilities.swift in Sources */, + B3BEB4C42A489A0A000751D5 /* WatchFaceBasics.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -857,18 +908,25 @@ files = ( B32243F02A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B3E1D6E42A0ACD7800F2905A /* WatchFaceView.swift in Sources */, + 9E6E10CE2AA6410A004CEDBE /* TaskGroup.swift in Sources */, + B3F85EB22A4A5F6900F8B40B /* DateTimeAdjust.swift in Sources */, + B38CC0492A4F1F1600F4DB9F /* WatchFace.swift in Sources */, B36D2F7A2A0483F800005162 /* WatchFaceBasics.swift in Sources */, B39086032A0314DD00943F2B /* ContentView.swift in Sources */, - B32243E22A0D3BF600E7AED5 /* WatchLayout.swift in Sources */, + B32243E22A0D3BF600E7AED5 /* Layout.swift in Sources */, + B329992F2A4F9C8500B71579 /* LocationManager.swift in Sources */, B39086182A0347DE00943F2B /* MetaLayout.swift in Sources */, - B3CC8BC12A0BCB730063DE44 /* Delegates.swift in Sources */, B3BFA2582A05E0590018F99E /* WatchConnectivity.swift in Sources */, - B395B5AA2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */, B39086172A0344ED00943F2B /* Data.swift in Sources */, - B39086012A0314DD00943F2B /* ChineseTimeApp.swift in Sources */, + B39086012A0314DD00943F2B /* watchApp.swift in Sources */, B39086142A0317CB00943F2B /* RoundedRect.swift in Sources */, + B3F85EAF2A4A5A0B00F8B40B /* HoverView.swift in Sources */, + 9E5A41212AA61FC300B470BE /* Relevance.swift in Sources */, + 9E0438CA2A8FD5D7007217A8 /* Locale.swift in Sources */, + 9E90D7F12A9EABD100855F2C /* Environments.swift in Sources */, B39086152A0344E500943F2B /* Model.swift in Sources */, - B3D318DE2A1661C2009FC18D /* ChineseTime.xcdatamodeld in Sources */, + B3F85EB42A4A5FA000F8B40B /* Setting.swift in Sources */, + 9EBFBE352A58A40900DC42AF /* ThemeData.swift in Sources */, B39086162A0344EA00943F2B /* PlanetModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -879,22 +937,27 @@ files = ( B3CC8BB32A0B31BE0063DE44 /* MetaLayout.swift in Sources */, B395B5A82A0F22CF003206E7 /* IconView.swift in Sources */, - B395B5AB2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */, + 9E5742812AA504D20052AE70 /* TaskGroup.swift in Sources */, B3CC8BB02A0B31B70063DE44 /* Model.swift in Sources */, + B34009482A352FEA003F50F7 /* WatchFaceView.swift in Sources */, B3CC8BB12A0B31B90063DE44 /* PlanetModel.swift in Sources */, + B383A6CF2A4D02D8002FADCF /* Single.swift in Sources */, B3CC8B9C2A0B30BB0063DE44 /* iOSWidgetBundle.swift in Sources */, - B3CC8BAE2A0B31970063DE44 /* Delegates.swift in Sources */, - B32243E92A0D8EAD00E7AED5 /* WatchWidget.swift in Sources */, - B3CC8BE02A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */, - B3CC8BAD2A0B31910063DE44 /* WidgetFaceView.swift in Sources */, + 9EBFBE372A58A40900DC42AF /* ThemeData.swift in Sources */, + B3E8A5192A4CF6BD00302473 /* CountDown.swift in Sources */, + B3E8A5162A4CF67700302473 /* Circular.swift in Sources */, + B383A6D22A4D02E2002FADCF /* Dual.swift in Sources */, + 9E0438CC2A8FD5D7007217A8 /* Locale.swift in Sources */, B3CC8BB52A0B323E0063DE44 /* WatchFaceBasics.swift in Sources */, - B32243EB2A0D8FD100E7AED5 /* WatchWidgetView.swift in Sources */, + B32243EB2A0D8FD100E7AED5 /* WatchWidgetBasic.swift in Sources */, B3CC8BB22A0B31BB0063DE44 /* Data.swift in Sources */, B32243F22A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, + B3970CF32A45066C0095F561 /* TextDesp.swift in Sources */, B3CC8BB42A0B31C20063DE44 /* RoundedRect.swift in Sources */, + B32999312A4F9C8700B71579 /* LocationManager.swift in Sources */, B3CC8BAF2A0B31B10063DE44 /* Layout.swift in Sources */, - B3CC8BB72A0B330C0063DE44 /* Widget.swift in Sources */, - B3D318DF2A1661C2009FC18D /* ChineseTime.xcdatamodeld in Sources */, + B383A6D52A4D1C7B002FADCF /* Relevance.swift in Sources */, + B3CC8BB72A0B330C0063DE44 /* Full.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -902,19 +965,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B3E8A51C2A4CF77000302473 /* Corner.swift in Sources */, + 9EBFBE382A58A40900DC42AF /* ThemeData.swift in Sources */, B38E96B12A0D3A82002FD662 /* Model.swift in Sources */, - B32243E32A0D3BF600E7AED5 /* WatchLayout.swift in Sources */, + B32243E32A0D3BF600E7AED5 /* Layout.swift in Sources */, + B3E8A51A2A4CF6BD00302473 /* CountDown.swift in Sources */, B38E96B62A0D3AA0002FD662 /* MetaLayout.swift in Sources */, B395B5A62A0F1A4A003206E7 /* IconView.swift in Sources */, B32243F32A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B38E96B42A0D3A8C002FD662 /* Data.swift in Sources */, - B3D318E02A1661C3009FC18D /* ChineseTime.xcdatamodeld in Sources */, + B32999322A4F9C8700B71579 /* LocationManager.swift in Sources */, B38E96B32A0D3A8A002FD662 /* PlanetModel.swift in Sources */, B32243E82A0D8E5000E7AED5 /* WatchWidgetBundle.swift in Sources */, - B395B5AC2A0F3A06003206E7 /* WatchWidget.intentdefinition in Sources */, - B38E96B52A0D3A97002FD662 /* Delegates.swift in Sources */, - B32243E52A0D8B6C00E7AED5 /* WatchWidgetView.swift in Sources */, - B3CC8BF32A0C7E300063DE44 /* WatchWidget.swift in Sources */, + B383A6D62A4D1C7B002FADCF /* Relevance.swift in Sources */, + B32243E52A0D8B6C00E7AED5 /* WatchWidgetBasic.swift in Sources */, + 9E5742822AA504D30052AE70 /* TaskGroup.swift in Sources */, + B3E8A5172A4CF67700302473 /* Circular.swift in Sources */, + B3CC8BF32A0C7E300063DE44 /* TextDesp.swift in Sources */, + 9E0438CD2A8FD5E0007217A8 /* Locale.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -922,18 +990,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B35097E12A0AEC9F001AB3CE /* WidgetFaceView.swift in Sources */, - B35097E22A0B1966001AB3CE /* ChineseTime.xcdatamodeld in Sources */, - B3CC8BDF2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */, + B383A6CE2A4D02D8002FADCF /* Single.swift in Sources */, + B32999302A4F9C8600B71579 /* LocationManager.swift in Sources */, B32243F12A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B3E1D6E12A0AC8E500F2905A /* MetaLayout.swift in Sources */, + 9EBFBE362A58A40900DC42AF /* ThemeData.swift in Sources */, B3E1D6E02A0AC89800F2905A /* Data.swift in Sources */, B3E1D6E22A0AC91700F2905A /* RoundedRect.swift in Sources */, B3E1D6CE2A0AB43E00F2905A /* MacWidgetBundle.swift in Sources */, B3E1D6E52A0ACDD800F2905A /* Layout.swift in Sources */, - B3E1D6E72A0ADA2800F2905A /* Delegates.swift in Sources */, B3E1D6DD2A0AC88B00F2905A /* WatchFaceBasics.swift in Sources */, - B3CC8BBD2A0B40E00063DE44 /* Widget.swift in Sources */, + 9E5742802AA504D20052AE70 /* TaskGroup.swift in Sources */, + B383A6D12A4D02E2002FADCF /* Dual.swift in Sources */, + B3CC8BBD2A0B40E00063DE44 /* Full.swift in Sources */, + B34009472A352FEA003F50F7 /* WatchFaceView.swift in Sources */, B3E1D6DE2A0AC89300F2905A /* Model.swift in Sources */, B3E1D6DF2A0AC89600F2905A /* PlanetModel.swift in Sources */, ); @@ -944,19 +1014,32 @@ buildActionMask = 2147483647; files = ( D2CFF74D270FF940000CECDA /* PlanetModel.swift in Sources */, - D2E4E0F926F7C908002F3716 /* WatchFace.swift in Sources */, - B3CC8BBF2A0BCB5E0063DE44 /* Delegates.swift in Sources */, D2F0825D26FAB23500ADBE13 /* Data.swift in Sources */, - B3CC8BDD2A0C7A3C0063DE44 /* Widget.intentdefinition in Sources */, - D2E4E0E626F7C73E002F3716 /* AppDelegate.swift in Sources */, - B34DA20C29FDFE3800562449 /* ViewController.swift in Sources */, - B37063A629FB088900CC6E57 /* MetaWatchFace.swift in Sources */, - D2E4E0EB26F7C73E002F3716 /* ChineseTime.xcdatamodeld in Sources */, + D2E4E0E626F7C73E002F3716 /* macApp.swift in Sources */, B37063A329FAFF3300CC6E57 /* MetaLayout.swift in Sources */, + 9EBFBE332A58A40900DC42AF /* ThemeData.swift in Sources */, + B32999242A4F989600B71579 /* SwiftUIUtilities.swift in Sources */, + B32999222A4F96D600B71579 /* Setting.swift in Sources */, + 9E0438C82A8FD5D7007217A8 /* Locale.swift in Sources */, + B32999292A4F9B7B00B71579 /* LocationManager.swift in Sources */, + 9ECDCA022A50B24800E11161 /* LayoutSetting.swift in Sources */, + 9E71FD072A50BF2E00C9CA78 /* WatchFace.swift in Sources */, D245D60926FA886200A89044 /* Model.swift in Sources */, D26CF1C026FD0C8D004EE9BB /* Layout.swift in Sources */, + 9E9889EF2A79EABF0066414A /* watchPanel.swift in Sources */, B34DA20929FDC0B200562449 /* Utilities.swift in Sources */, + 9ECDCA052A50B6D100E11161 /* Documentation.swift in Sources */, + 9E90D7EF2A9EABD100855F2C /* Environments.swift in Sources */, + 9ECDCA032A50B6CB00E11161 /* ColorSetting.swift in Sources */, + 9E71FD042A50BD2A00C9CA78 /* Location.swift in Sources */, + 9E71FD022A50BB0F00C9CA78 /* ThemesList.swift in Sources */, + B3F85EAD2A4A5A0B00F8B40B /* HoverView.swift in Sources */, + 9E71FD032A50BB3900C9CA78 /* Datetime.swift in Sources */, + B32999262A4F9B2500B71579 /* WatchFaceView.swift in Sources */, + 9E820BE72A7DEA1700453389 /* Welcome.swift in Sources */, B39086122A0317CB00943F2B /* RoundedRect.swift in Sources */, + B32999252A4F9AB100B71579 /* WatchFaceBasics.swift in Sources */, + 9ECDCA042A50B6CE00E11161 /* RingSetting.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -985,192 +1068,13 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - B33CC9BE29FB583B00426C92 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - B3FE725B29FB5E3A009554C2 /* Base */, - B3FE725C29FB5E42009554C2 /* zh-Hant */, - B3FE725D29FB5E45009554C2 /* zh-Hans */, - B3FE725E29FB5E47009554C2 /* en */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - B395B5AE2A0F3A06003206E7 /* WatchWidget.intentdefinition */ = { - isa = PBXVariantGroup; - children = ( - B395B5AD2A0F3A06003206E7 /* Base */, - B395B5B02A0F3A38003206E7 /* zh-Hant */, - B395B5B22A0F3A39003206E7 /* zh-Hans */, - B395B5B42A0F3A3A003206E7 /* en */, - ); - name = WatchWidget.intentdefinition; - sourceTree = ""; - }; - B3AF0B8C29FB658600C78081 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3AF0B8D29FB658600C78081 /* en */, - B3AF0B9829FB6C6400C78081 /* zh-Hans */, - B3AF0B9C29FB6C6D00C78081 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - B3AF0B8F29FB658600C78081 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3AF0B9029FB658600C78081 /* en */, - B3AF0B9929FB6C6400C78081 /* zh-Hans */, - B379AD6D29FF0C03009C373E /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - B3AF0B9229FB658600C78081 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3AF0B9329FB658600C78081 /* en */, - B3AF0B9A29FB6C6400C78081 /* zh-Hans */, - B3AF0B9D29FB6C6D00C78081 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - B3AF0B9529FB658600C78081 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3AF0B9629FB658600C78081 /* en */, - B3AF0B9B29FB6C6400C78081 /* zh-Hans */, - B3AF0B9E29FB6C6D00C78081 /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - B3BFA25C2A06BBA60018F99E /* ChineseTime Watch App-InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3BFA25D2A06BBA60018F99E /* en */, - B3BFA2622A06BBAF0018F99E /* zh-Hans */, - B3BFA2642A06BBB50018F99E /* zh-Hant */, - ); - name = "ChineseTime Watch App-InfoPlist.strings"; - sourceTree = ""; - }; - B3BFA25F2A06BBA60018F99E /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3BFA2602A06BBA60018F99E /* en */, - B3BFA2632A06BBAF0018F99E /* zh-Hans */, - B3D2394A2A0F463000E506EB /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - B3CC8BC52A0C7A090063DE44 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BC62A0C7A090063DE44 /* en */, - B3CC8BD12A0C7A1F0063DE44 /* zh-Hans */, - B3CC8BD82A0C7A2A0063DE44 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - B3CC8BC82A0C7A090063DE44 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BC92A0C7A090063DE44 /* en */, - B3CC8BD22A0C7A1F0063DE44 /* zh-Hans */, - B3CC8BD92A0C7A2A0063DE44 /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - B3CC8BCB2A0C7A090063DE44 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BCC2A0C7A090063DE44 /* en */, - B3CC8BD32A0C7A1F0063DE44 /* zh-Hans */, - B3CC8BDA2A0C7A2A0063DE44 /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - B3CC8BCE2A0C7A090063DE44 /* Chinese-Time-Watch-InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BCF2A0C7A090063DE44 /* en */, - B3CC8BD72A0C7A1F0063DE44 /* zh-Hans */, - B3CC8BDC2A0C7A2A0063DE44 /* zh-Hant */, - ); - name = "Chinese-Time-Watch-InfoPlist.strings"; - sourceTree = ""; - }; - B3CC8BD42A0C7A1F0063DE44 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BD52A0C7A1F0063DE44 /* zh-Hans */, - B3CC8BDB2A0C7A2A0063DE44 /* zh-Hant */, - B3CC8BE92A0C7CEB0063DE44 /* en */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - B3CC8BE22A0C7A3C0063DE44 /* Widget.intentdefinition */ = { - isa = PBXVariantGroup; - children = ( - B3CC8BE12A0C7A3C0063DE44 /* Base */, - B3CC8BE42A0C7A400063DE44 /* zh-Hant */, - B3CC8BE62A0C7A410063DE44 /* zh-Hans */, - B3CC8BE82A0C7A410063DE44 /* en */, - ); - name = Widget.intentdefinition; - sourceTree = ""; - }; - B3D239422A0F461C00E506EB /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - B3D239432A0F461C00E506EB /* en */, - B3D239482A0F462600E506EB /* zh-Hans */, - B3D2394B2A0F463000E506EB /* zh-Hant */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - B3D239452A0F461C00E506EB /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - B3D239462A0F461C00E506EB /* en */, - B3D239492A0F462600E506EB /* zh-Hans */, - B3D2394C2A0F463000E506EB /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - D2633071270CF85F0053B9F6 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - B3FE725729FB5E0C009554C2 /* zh-Hans */, - B3FE725829FB5E0E009554C2 /* en */, - B3FE725929FB5E14009554C2 /* Base */, - B3FE725A29FB5E31009554C2 /* zh-Hant */, - ); - name = Main.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ B3515CD629F6147200E6BCDC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "iOS/Chinese Time iOS.entitlements"; + CODE_SIGN_ENTITLEMENTS = iOS/ChineseTimeiOS.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEVELOPMENT_TEAM = 28HU5A7B46; @@ -1179,25 +1083,24 @@ INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1207,11 +1110,9 @@ B3515CD729F6147200E6BCDC /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = "iOS/Chinese Time iOS.entitlements"; + CODE_SIGN_ENTITLEMENTS = iOS/ChineseTimeiOS.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEVELOPMENT_TEAM = 28HU5A7B46; @@ -1220,25 +1121,24 @@ INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1249,21 +1149,21 @@ B390860D2A0314DD00943F2B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "Watch/Chinese Time Watch.entitlements"; + CODE_SIGN_ENTITLEMENTS = Watch/ChineseTimeWatch.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_VERSION; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 28HU5A7B46; ENABLE_PREVIEWS = NO; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Watch/Chinese-Time-Watch-Info.plist"; + INFOPLIST_FILE = Watch/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "Yuncao-Liu.ChineseTime"; INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; @@ -1280,28 +1180,28 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Debug; }; B390860E2A0314DD00943F2B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "Watch/Chinese Time Watch.entitlements"; + CODE_SIGN_ENTITLEMENTS = Watch/ChineseTimeWatch.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_VERSION; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 28HU5A7B46; ENABLE_PREVIEWS = NO; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Watch/Chinese-Time-Watch-Info.plist"; + INFOPLIST_FILE = Watch/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "Yuncao-Liu.ChineseTime"; INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; @@ -1319,7 +1219,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Release; }; @@ -1328,18 +1228,18 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = iOSWidget/iOSWidgetExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = iOSWidget/iOSWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_BUILD; DEVELOPMENT_TEAM = 28HU5A7B46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iOSWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = iOSWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1348,9 +1248,13 @@ MARKETING_VERSION = $APP_VERSION; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.iOSWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1362,18 +1266,18 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = iOSWidget/iOSWidgetExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = iOSWidget/iOSWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_BUILD; DEVELOPMENT_TEAM = 28HU5A7B46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iOSWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = iOSWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1382,9 +1286,13 @@ MARKETING_VERSION = $APP_VERSION; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.iOSWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1397,17 +1305,17 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidgetExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_VERSION; DEVELOPMENT_TEAM = 28HU5A7B46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WatchWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = WatchWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1416,13 +1324,13 @@ ); MARKETING_VERSION = $APP_BUILD; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.Watch.WatchWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Debug; }; @@ -1431,17 +1339,17 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidgetExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = WatchWidget/WatchWidget.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_VERSION; DEVELOPMENT_TEAM = 28HU5A7B46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WatchWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = WatchWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1450,14 +1358,14 @@ ); MARKETING_VERSION = $APP_BUILD; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.Watch.WatchWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Release; }; @@ -1467,30 +1375,28 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = MacWidget/MacWidget.entitlements; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_BUILD; DEVELOPMENT_TEAM = 28HU5A7B46; - ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MacWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = MacWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = $APP_VERSION; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.MacWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SKIP_INSTALL = YES; + STRINGS_FILE_OUTPUT_ENCODING = binary; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -1502,30 +1408,29 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = MacWidget/MacWidget.entitlements; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = $APP_BUILD; DEVELOPMENT_TEAM = 28HU5A7B46; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MacWidget/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = MacWidget; + INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time Widget"; INFOPLIST_KEY_NSHumanReadableCopyright = "Open source under GPL v3"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse."; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = $APP_VERSION; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime.MacWidget"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time Widget"; SKIP_INSTALL = YES; + STRINGS_FILE_OUTPUT_ENCODING = binary; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -1535,12 +1440,16 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 80; - APP_VERSION = 4.2.5; + APP_BUILD = 95; + APP_VERSION = 5.0; + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_OPTIMIZATION = space; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++23"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -1573,7 +1482,8 @@ DEPLOYMENT_LOCATION = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -1583,19 +1493,26 @@ ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MACOSX_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = binary; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Debug; }; @@ -1603,12 +1520,16 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 80; - APP_VERSION = 4.2.5; + APP_BUILD = 95; + APP_VERSION = 5.0; + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_OPTIMIZATION = space; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++23"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -1635,48 +1556,51 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEPLOYMENT_LOCATION = NO; + DEPLOYMENT_POSTPROCESSING = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MACOSX_DEPLOYMENT_TARGET = 12.0; + 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; + PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = binary; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - WATCHOS_DEPLOYMENT_TARGET = 9.0; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Release; }; D2E4E0F626F7C73F002F3716 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = macOS/ChineseTime.entitlements; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_ENTITLEMENTS = macOS/ChineseTimeMac.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 28HU5A7B46; - ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -1684,10 +1608,12 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time"; + STRINGS_FILE_OUTPUT_ENCODING = binary; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; @@ -1695,17 +1621,15 @@ D2E4E0F726F7C73F002F3716 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = macOS/ChineseTime.entitlements; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_ENTITLEMENTS = macOS/ChineseTimeMac.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 28HU5A7B46; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Chinese Time"; @@ -1714,10 +1638,12 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "Yuncao-Liu.ChineseTime"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Chinese Time"; + STRINGS_FILE_OUTPUT_ENCODING = binary; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; @@ -1779,7 +1705,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D2E4E0F526F7C73F002F3716 /* Build configuration list for PBXNativeTarget "Chinese Time" */ = { + D2E4E0F526F7C73F002F3716 /* Build configuration list for PBXNativeTarget "Chinese Time Mac" */ = { isa = XCConfigurationList; buildConfigurations = ( D2E4E0F626F7C73F002F3716 /* Debug */, @@ -1789,19 +1715,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCVersionGroup section */ - D2E4E0E926F7C73E002F3716 /* ChineseTime.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - D2E4E0EA26F7C73E002F3716 /* ChineseTime.xcdatamodel */, - ); - currentVersion = D2E4E0EA26F7C73E002F3716 /* ChineseTime.xcdatamodel */; - path = ChineseTime.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ }; rootObject = D2E4E0DA26F7C73E002F3716 /* Project object */; } diff --git a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time.xcscheme b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Mac.xcscheme similarity index 77% rename from Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time.xcscheme rename to Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Mac.xcscheme index 118c4a2..610c7d7 100644 --- a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time.xcscheme +++ b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Mac.xcscheme @@ -1,6 +1,6 @@ @@ -48,13 +48,29 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D2E4E0E126F7C73E002F3716" BuildableName = "Chinese Time.app" - BlueprintName = "Chinese Time" + BlueprintName = "Chinese Time Mac" ReferencedContainer = "container:Chinese Time.xcodeproj"> + + + + + + + + @@ -71,7 +87,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D2E4E0E126F7C73E002F3716" BuildableName = "Chinese Time.app" - BlueprintName = "Chinese Time" + BlueprintName = "Chinese Time Mac" ReferencedContainer = "container:Chinese Time.xcodeproj"> diff --git a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Watch.xcscheme b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Watch.xcscheme index c9209b1..a69a103 100644 --- a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Watch.xcscheme +++ b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Chinese Time Watch.xcscheme @@ -1,6 +1,6 @@ @@ -52,6 +52,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> @@ -63,6 +64,28 @@ ReferencedContainer = "container:Chinese Time.xcodeproj"> + + + + + + + + + + + + @@ -44,11 +44,33 @@ + + + + + + + + + + + + diff --git a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Mac Widget Extension.xcscheme b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Mac Widget Extension.xcscheme index e684b39..6aa8d59 100644 --- a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Mac Widget Extension.xcscheme +++ b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Mac Widget Extension.xcscheme @@ -1,6 +1,6 @@ @@ -30,7 +30,7 @@ @@ -46,8 +46,8 @@ - - - - + value = "systemExtraLarge" + isEnabled = "YES"> diff --git a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Watch Widget Extension.xcscheme b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Watch Widget Extension.xcscheme index 15e1941..e026fec 100644 --- a/Chinese Time.xcodeproj/xcshareddata/xcschemes/Watch Widget Extension.xcscheme +++ b/Chinese Time.xcodeproj/xcshareddata/xcschemes/Watch Widget Extension.xcscheme @@ -1,6 +1,6 @@ @@ -43,7 +43,7 @@ @@ -74,7 +74,7 @@ @@ -83,7 +83,7 @@ @@ -91,8 +91,8 @@ + value = "Card" + isEnabled = "YES"> + value = "accessoryRectangular" + isEnabled = "YES"> @@ -117,7 +117,7 @@ diff --git a/Chinese Time.xcodeproj/xcshareddata/xcschemes/iOS Widget Extension.xcscheme b/Chinese Time.xcodeproj/xcshareddata/xcschemes/iOS Widget Extension.xcscheme index c57f582..e617cfc 100644 --- a/Chinese Time.xcodeproj/xcshareddata/xcschemes/iOS Widget Extension.xcscheme +++ b/Chinese Time.xcodeproj/xcshareddata/xcschemes/iOS Widget Extension.xcscheme @@ -1,6 +1,6 @@ @@ -30,7 +30,7 @@ @@ -62,7 +62,7 @@ @@ -71,7 +71,7 @@ @@ -89,8 +89,8 @@ + value = "systemMedium" + isEnabled = "YES"> @@ -107,7 +107,7 @@ diff --git a/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 66fe9f1..4d008e7 100644 --- a/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,4 +3,102 @@ uuid = "17FF20B8-95CE-4A9A-9D52-A57C1B74571D" type = "1" version = "2.0"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcschemes/xcschememanagement.plist b/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcschemes/xcschememanagement.plist index 5666b94..fa8e9e7 100644 --- a/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Chinese Time.xcodeproj/xcuserdata/leo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + Chinese Time Mac.xcscheme_^#shared#^_ + + orderHint + 0 + Chinese Time Watch.xcscheme_^#shared#^_ orderHint @@ -14,7 +19,7 @@ orderHint 1 - Chinese Time.xcscheme_^#shared#^_ + Chinese Time mac.xcscheme_^#shared#^_ orderHint 0 diff --git a/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..c2b17a0 --- /dev/null +++ b/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcschemes/xcschememanagement.plist b/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..aa53b89 --- /dev/null +++ b/Chinese Time.xcodeproj/xcuserdata/leoliu.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,39 @@ + + + + + SchemeUserState + + Chinese Time Mac.xcscheme_^#shared#^_ + + orderHint + 0 + + Chinese Time Watch.xcscheme_^#shared#^_ + + orderHint + 2 + + Chinese Time iOS.xcscheme_^#shared#^_ + + orderHint + 1 + + Mac Widget Extension.xcscheme_^#shared#^_ + + orderHint + 3 + + Watch Widget Extension.xcscheme_^#shared#^_ + + orderHint + 5 + + iOS Widget Extension.xcscheme_^#shared#^_ + + orderHint + 4 + + + + diff --git a/MacWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/MacWidget/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 3f00db4..0000000 --- a/MacWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "images" : [ - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MacWidget/Assets.xcassets/Contents.json b/MacWidget/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/MacWidget/Assets.xcassets/Contents.json +++ b/MacWidget/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/MacWidget/Info.plist b/MacWidget/Info.plist index f8bc077..d40f568 100644 --- a/MacWidget/Info.plist +++ b/MacWidget/Info.plist @@ -8,6 +8,8 @@ $(TeamIdentifierPrefix)ChineseTime ITSAppUsesNonExemptEncryption + LSHasLocalizedDisplayName + NSExtension NSExtensionPointIdentifier diff --git a/MacWidget/MacWidgetBundle.swift b/MacWidget/MacWidgetBundle.swift index faea024..9ccd814 100644 --- a/MacWidget/MacWidgetBundle.swift +++ b/MacWidget/MacWidgetBundle.swift @@ -9,12 +9,7 @@ import SwiftUI @main struct MacWidgetBundle: WidgetBundle { - init() { - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - } - @WidgetBundleBuilder var body: some Widget { SmallWidget() MediumWidget() diff --git a/MacWidget/en.lproj/InfoPlist.strings b/MacWidget/en.lproj/InfoPlist.strings deleted file mode 100644 index 73d0aa4..0000000 --- a/MacWidget/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "Chinese Time Widget"; - -/* Bundle name */ -"CFBundleName" = "Mac Widget Extension"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/MacWidget/en.lproj/Localizable.strings b/MacWidget/en.lproj/Localizable.strings deleted file mode 100644 index 6336d8e..0000000 --- a/MacWidget/en.lproj/Localizable.strings +++ /dev/null @@ -1,24 +0,0 @@ -/* No comment provided by engineer. */ -"Compact" = "Compact"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "Compact watch face to display either Date or Time."; - -/* Default save file name */ -"Default" = "Default"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "Display both Date and Time as separate watches, whose order is at your choice."; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "Display full information with both Date and Time."; - -/* No comment provided by engineer. */ -"Dual" = "Dual"; - -/* No comment provided by engineer. */ -"Full" = "Complete"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - diff --git a/MacWidget/zh-Hans.lproj/InfoPlist.strings b/MacWidget/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 9ba3821..0000000 --- a/MacWidget/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "华历挂件"; - -/* Bundle name */ -"CFBundleName" = "电脑挂件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/MacWidget/zh-Hans.lproj/Localizable.strings b/MacWidget/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index f0b7ba1..0000000 --- a/MacWidget/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,24 +0,0 @@ -/* No comment provided by engineer. */ -"Compact" = "半"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "日、时二择一"; - -/* Default save file name */ -"Default" = "常备"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "日、时分列,顺序可选"; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "完整展示"; - -/* No comment provided by engineer. */ -"Dual" = "双"; - -/* No comment provided by engineer. */ -"Full" = "完"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - diff --git a/MacWidget/zh-Hant.lproj/InfoPlist.strings b/MacWidget/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 6021f64..0000000 --- a/MacWidget/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "華曆掛件"; - -/* Bundle name */ -"CFBundleName" = "電腦掛件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - diff --git a/MacWidget/zh-Hant.lproj/Localizable.strings b/MacWidget/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 2a4ea93..0000000 --- a/MacWidget/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,21 +0,0 @@ -/* No comment provided by engineer. */ -"Compact" = "半"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "日、時二擇一"; - -/* Default save file name */ -"Default" = "常備"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "日、時分列,順序可選"; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "完整展示"; - -/* No comment provided by engineer. */ -"Dual" = "雙"; - -/* No comment provided by engineer. */ -"Full" = "完"; - diff --git a/README.md b/README.md index 73a09be..8fc5d49 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ For detailed information about the background, design (screenshots), and feature This app is available on `macOS`, `iOS`, `iPadOS`, and `watchOS`, and includes widgets for all these platforms. To build the project from the source, simply download or clone this repo, then build with Xcode. The app does not rely on any third-party dependencies. -Minimum OS requirements: `macOS 12.0`, `iOS/iPadOS 15.0`, `watchOS 9.0`. You will need `Xcode 14.0` or later to build from the source. +Minimum OS requirements: `macOS 14.0`, `iOS/iPadOS 17.0`, `watchOS 10.0`. You will need `Xcode 15.0` or later to build from the source. ## Contributing diff --git a/Shared/ChineseTime.xcdatamodeld/.xccurrentversion b/Shared/ChineseTime.xcdatamodeld/.xccurrentversion deleted file mode 100644 index fe2d8d3..0000000 --- a/Shared/ChineseTime.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,8 +0,0 @@ - - - - - _XCCurrentVersionName - ChineseTime.xcdatamodel - - diff --git a/Shared/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents b/Shared/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents deleted file mode 100644 index 211f48c..0000000 --- a/Shared/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Shared/Data.swift b/Shared/DataModel/Data.swift similarity index 100% rename from Shared/Data.swift rename to Shared/DataModel/Data.swift diff --git a/Shared/DataModel/LocationManager.swift b/Shared/DataModel/LocationManager.swift new file mode 100644 index 0000000..4832805 --- /dev/null +++ b/Shared/DataModel/LocationManager.swift @@ -0,0 +1,147 @@ +// +// Delegates.swift +// MacWidgetExtension +// +// Created by Leo Liu on 5/9/23. +// + +import CoreLocation +import Observation + +@Observable final class LocationManager: NSObject, CLLocationManagerDelegate { + static let shared = LocationManager() + + private var _location: CGPoint? = nil + private(set) var location: CGPoint? { + get { + if enabled { + _location + } else { + nil + } + } set { + _location = newValue + if newValue == nil { + lastUpdated = Date.distantPast + } else { + lastUpdated = Date.now + } + } + } + + @ObservationIgnored let manager = CLLocationManager() + @ObservationIgnored private var lastUpdated = Date.distantPast + @ObservationIgnored private var continuation: CheckedContinuation? + @ObservationIgnored let watchLayout = WatchLayout.shared + var enabled: Bool { + get { + if watchLayout.locationEnabled { + switch manager.authorizationStatus { +#if os(macOS) + case .authorized, .authorizedAlways: // Location services are available. + return true +#else + case .authorizedWhenInUse, .authorizedAlways: // Location services are available. + return true +#endif + case .restricted, .denied: + return false + case .notDetermined: // Authorization not determined yet. + return false + @unknown default: + return false + } + } else { + return false + } + } set { + watchLayout.locationEnabled = newValue + if newValue { + requestLocation() + } + } + } + + + override private init() { + super.init() + manager.delegate = self + manager.requestWhenInUseAuthorization() + manager.desiredAccuracy = kCLLocationAccuracyKilometer + } + + func requestLocation() { + if enabled && (lastUpdated.distance(to: .now) > 3600) { + switch manager.authorizationStatus { +#if os(macOS) + case .authorized, .authorizedAlways: + manager.requestLocation() +#else + case .authorizedWhenInUse, .authorizedAlways: + manager.requestLocation() +#endif + case .notDetermined: // Authorization not determined yet. + manager.requestWhenInUseAuthorization() + default: + return + } + } + } + + func getLocation() async -> CGPoint? { + if enabled && (lastUpdated.distance(to: .now) > 3600) { +#if os(watchOS) + let authorized = [.authorizedWhenInUse, .authorizedAlways].contains(manager.authorizationStatus) +#else + let authorized = manager.isAuthorizedForWidgetUpdates +#endif + if authorized { + return await withCheckedContinuation { continuation in + self.continuation = continuation + manager.requestLocation() + } + } + } + return nil + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let location = locations.last { + let locationPoint = CGPoint(x: location.coordinate.latitude, y: location.coordinate.longitude) + self.location = locationPoint + continuation?.resume(with: .success(locationPoint)) + } else { + continuation?.resume(with: .success(nil)) + } + continuation = nil + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { +#if os(macOS) + case .authorized, .authorizedAlways: // Location services are available. + requestLocation() +#else + case .authorizedWhenInUse, .authorizedAlways: // Location services are available. + requestLocation() +#endif + + case .restricted, .denied: + break + + case .notDetermined: // Authorization not determined yet. + manager.requestWhenInUseAuthorization() + + @unknown default: + print("Unhandled Location Authorization Case") + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + if let continuation = continuation { + continuation.resume(with: .success(nil)) + } + continuation = nil + print(error) + } +} diff --git a/Shared/DataModel/MetaLayout.swift b/Shared/DataModel/MetaLayout.swift new file mode 100644 index 0000000..d0bca07 --- /dev/null +++ b/Shared/DataModel/MetaLayout.swift @@ -0,0 +1,464 @@ +// +// Layout.swift +// Chinese Time +// +// Created by Leo Liu on 4/27/23. +// + +import CoreGraphics +import Foundation +import Observation +import SwiftData +import SwiftUI + +private let displayP3 = CGColorSpace(name: CGColorSpace.displayP3)! + +extension CGColor { + var hexCode: String { + var colorString = "0x" + let colorWithColorspace = converted(to: displayP3, intent: .defaultIntent, options: nil) ?? self + colorString += String(format: "%02X", Int(round(colorWithColorspace.components![3] * 255))) + colorString += String(format: "%02X", Int(round(colorWithColorspace.components![2] * 255))) + colorString += String(format: "%02X", Int(round(colorWithColorspace.components![1] * 255))) + colorString += String(format: "%02X", Int(round(colorWithColorspace.components![0] * 255))) + return colorString + } +} + +extension CGPoint { + func encode() -> String { + return "x: \(x), y: \(y)" + } + + init?(from str: String?) { + self.init() + guard let str = str else { return nil } + let regex = /x:\s*([\-0-9\.]+)\s*,\s*y:\s*([\-0-9\.]+)/ + let matches = try? regex.firstMatch(in: str)?.output + if let matches = matches, let x = Double(matches.1), let y = Double(matches.2) { + self.x = x + self.y = y + } else { + return nil + } + } +} + +protocol OptionalType { + associatedtype Wrapped + var optional: Wrapped? { get } +} + +extension Optional: OptionalType { + var optional: Self { self } +} + +extension Array where Element: OptionalType { + func flattened() -> [Element.Wrapped]? { + var newArray = [Element.Wrapped]() + for item in self { + if item.optional == nil { + return nil + } else { + newArray.append(item.optional!) + } + } + return newArray + } +} + +extension String { + var intValue: Int? { + let string = trimmingCharacters(in: .whitespaces) + if string.isEmpty { + return nil + } else { + return Int(string) + } + } + + var floatValue: CGFloat? { + let string = trimmingCharacters(in: .whitespaces) + if string.isEmpty { + return nil + } else { + return Double(string).map { CGFloat($0) } + } + } + + var boolValue: Bool? { + guard !isEmpty else { + return nil + } + let trimmedString = trimmingCharacters(in: .whitespaces).lowercased() + if ["true", "yes"].contains(trimmedString) { + return true + } else if ["false", "no"].contains(trimmedString) { + return false + } else { + return nil + } + } + + var colorValue: CGColor? { + let string = trimmingCharacters(in: .whitespaces) + guard !string.isEmpty else { + return nil + } + var r = 0, g = 0, b = 0, a = 0xff + if string.count == 10 { + // 0xffccbbaa + let regex = /^0x([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ + let matches = try? regex.firstMatch(in: string)?.output + if let matches = matches { + r = Int(matches.4, radix: 16)! + g = Int(matches.3, radix: 16)! + b = Int(matches.2, radix: 16)! + a = Int(matches.1, radix: 16)! + } else { + return nil + } + } else if string.count == 8 { + // 0xccbbaa + let regex = /^0x([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ + let matches = try? regex.firstMatch(in: string)?.output + if let matches = matches { + r = Int(matches.3, radix: 16)! + g = Int(matches.2, radix: 16)! + b = Int(matches.1, radix: 16)! + } else { + return nil + } + } + return CGColor(colorSpace: displayP3, components: [CGFloat(r)/255, CGFloat(g)/255, CGFloat(b)/255, CGFloat(a)/255]) + } +} + +@Observable class MetaWatchLayout { + @Observable final class Gradient { + private let _locations: [CGFloat] + private let _colors: [CGColor] + let isLoop: Bool + + init(locations: [CGFloat], colors: [CGColor], loop: Bool) { + guard locations.count == colors.count else { + fatalError() + } + var colorAndLocation = Array(zip(colors, locations)) + colorAndLocation.sort { former, latter in + former.1 < latter.1 + } + _locations = colorAndLocation.map { $0.1 } + _colors = colorAndLocation.map { $0.0 } + isLoop = loop + } + + func interpolate(at: CGFloat) -> CGColor { + let locations = self.locations + let colors = self.colors + let nextIndex = locations.firstIndex { $0 >= at } + if let nextIndex = nextIndex { + let previousIndex = nextIndex.advanced(by: -1) + if previousIndex >= locations.startIndex { + let leftColor = colors[previousIndex] + let rightColor = colors[nextIndex] + var ratio = (at - locations[previousIndex])/(locations[nextIndex] - locations[previousIndex]) + ratio = max(0.0, min(1.0, ratio)) + let leftComponents = leftColor.converted(to: displayP3, intent: .perceptual, options: nil)!.components! + let rightComponents = rightColor.converted(to: displayP3, intent: .perceptual, options: nil)!.components! + let newComponents = zip(leftComponents, rightComponents).map { (1 - ratio) * $0.0 + ratio * $0.1 } + let newColor = CGColor(colorSpace: displayP3, components: newComponents) + return newColor! + } else { + return colors.first! + } + } else { + return colors.last! + } + } + + var locations: [CGFloat] { + if isLoop { + return _locations + [1] + } else { + return _locations + } + } + + var colors: [CGColor] { + if isLoop { + return _colors + [_colors[0]] + } else { + return _colors + } + } + + func encode() -> String { + var encoded = "" + let locationString = _locations.map { "\($0)" }.joined(separator: ", ") + encoded += "locations: \(locationString)\n" + let colorString = _colors.map { $0.hexCode }.joined(separator: ", ") + encoded += "colors: \(colorString)\n" + encoded += "loop: \(isLoop)" + return encoded + } + + init?(from str: String?) { + guard let str = str else { return nil } + let regex = /([a-zA-Z_0-9]+)\s*:[\s"]*([^\s"#][^"#]*)[\s"#]*(#*.*)$/ + var values = [String: String]() + for line in str.split(whereSeparator: \.isNewline) { + if let match = try? regex.firstMatch(in: String(line))?.output { + values[String(match.1)] = String(match.2) + } + } + guard let newLocations = values["locations"]?.split(separator: ","), let newColors = values["colors"]?.split(separator: ","), let isLoop = values["loop"]?.boolValue else { return nil } + let locations = Array(newLocations).map { $0.trimmingCharacters(in: .whitespaces).floatValue } + let colors = Array(newColors).map { $0.trimmingCharacters(in: .whitespaces).colorValue } + guard let loc = locations.flattened(), let col = colors.flattened() else { return nil } + _locations = loc + _colors = col + self.isLoop = isLoop + } + } + + var globalMonth: Bool = false + var apparentTime: 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) + var secondRing = Gradient(locations: [0, 1], colors: [CGColor(gray: 1, alpha: 1), CGColor(gray: 1, alpha: 1)], loop: false) + var thirdRing = Gradient(locations: [0, 1], colors: [CGColor(gray: 1, alpha: 1), CGColor(gray: 1, alpha: 1)], loop: false) + var innerColor = CGColor(gray: 0, alpha: 0) + var majorTickColor = CGColor(gray: 0, alpha: 0) + var minorTickColor = CGColor(gray: 0, alpha: 0) + var majorTickAlpha: CGFloat = 0 + var minorTickAlpha: CGFloat = 0.7 + var fontColor = CGColor(gray: 0, alpha: 1) + var centerFontColor = Gradient(locations: [0, 1], colors: [CGColor(gray: 1, alpha: 1), CGColor(gray: 1, alpha: 1)], loop: false) + var evenSolarTermTickColor = CGColor(gray: 1, alpha: 1) + var oddSolarTermTickColor = CGColor(gray: 0.67, alpha: 1) + var innerColorDark = CGColor(gray: 0, alpha: 0) + var majorTickColorDark = CGColor(gray: 0, alpha: 0) + var minorTickColorDark = CGColor(gray: 0, alpha: 0) + var fontColorDark = CGColor(gray: 0, alpha: 1) + var evenSolarTermTickColorDark = CGColor(gray: 1, alpha: 1) + var oddSolarTermTickColorDark = CGColor(gray: 0.67, alpha: 1) + var planetIndicator: [CGColor] = [CGColor(gray: 0.2, alpha: 1), CGColor(gray: 0.3, alpha: 1), CGColor(gray: 0.4, alpha: 1), CGColor(gray: 0.5, alpha: 1), CGColor(gray: 0.6, alpha: 1), CGColor(gray: 0.7, alpha: 1)] + var sunPositionIndicator: [CGColor] = [CGColor(gray: 0.3, alpha: 1), CGColor(gray: 0.4, alpha: 1), CGColor(gray: 0.5, alpha: 1), CGColor(gray: 0.6, alpha: 1)] + var moonPositionIndicator: [CGColor] = [CGColor(gray: 0.4, alpha: 1), CGColor(gray: 0.5, alpha: 1), CGColor(gray: 0.6, alpha: 1)] + var eclipseIndicator = CGColor(gray: 0.3, alpha: 1) + var fullmoonIndicator = CGColor(gray: 0.4, alpha: 1) + var oddStermIndicator = CGColor(gray: 0.5, alpha: 1) + var evenStermIndicator = CGColor(gray: 0.6, alpha: 1) + var shadeAlpha: CGFloat = 0 + var shadowSize: CGFloat = 0.03 + var centerTextOffset: CGFloat = 0 + var centerTextHOffset: CGFloat = 0 + var verticalTextOffset: CGFloat = 0 + var horizontalTextOffset: CGFloat = 0 + var watchSize: CGSize = .zero + var cornerRadiusRatio: CGFloat = 0 + + func encode(includeOffset: Bool = true, includeColor: Bool = true, includeConfig: Bool = true) -> String { + var encoded = "" + if includeConfig { + encoded += "globalMonth: \(globalMonth)\n" + encoded += "apparentTime: \(apparentTime)\n" + encoded += "locationEnabled: \(locationEnabled)\n" + if let location = location { + encoded += "customLocation: \(location.encode())\n" + } + } + if includeColor { + encoded += "firstRing: \(firstRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" + encoded += "secondRing: \(secondRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" + encoded += "thirdRing: \(thirdRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" + encoded += "innerColor: \(innerColor.hexCode)\n" + encoded += "majorTickColor: \(majorTickColor.hexCode)\n" + encoded += "majorTickAlpha: \(majorTickAlpha)\n" + encoded += "minorTickColor: \(minorTickColor.hexCode)\n" + encoded += "minorTickAlpha: \(minorTickAlpha)\n" + encoded += "fontColor: \(fontColor.hexCode)\n" + encoded += "centerFontColor: \(centerFontColor.encode().replacingOccurrences(of: "\n", with: "; "))\n" + encoded += "evenSolarTermTickColor: \(evenSolarTermTickColor.hexCode)\n" + encoded += "oddSolarTermTickColor: \(oddSolarTermTickColor.hexCode)\n" + encoded += "innerColorDark: \(innerColorDark.hexCode)\n" + encoded += "majorTickColorDark: \(majorTickColorDark.hexCode)\n" + encoded += "minorTickColorDark: \(minorTickColorDark.hexCode)\n" + encoded += "fontColorDark: \(fontColorDark.hexCode)\n" + encoded += "evenSolarTermTickColorDark: \(evenSolarTermTickColorDark.hexCode)\n" + encoded += "oddSolarTermTickColorDark: \(oddSolarTermTickColorDark.hexCode)\n" + encoded += "planetIndicator: \(planetIndicator.map { $0.hexCode }.joined(separator: ", "))\n" + encoded += "eclipseIndicator: \(eclipseIndicator.hexCode)\n" + encoded += "fullmoonIndicator: \(fullmoonIndicator.hexCode)\n" + encoded += "oddStermIndicator: \(oddStermIndicator.hexCode)\n" + encoded += "evenStermIndicator: \(evenStermIndicator.hexCode)\n" + encoded += "sunPositionIndicator: \(sunPositionIndicator.map { $0.hexCode }.joined(separator: ", "))\n" + encoded += "moonPositionIndicator: \(moonPositionIndicator.map { $0.hexCode }.joined(separator: ", "))\n" + encoded += "shadeAlpha: \(shadeAlpha)\n" + encoded += "shadowSize: \(shadowSize)\n" + } + if includeOffset { + encoded += "centerTextOffset: \(centerTextOffset)\n" + encoded += "centerTextHorizontalOffset: \(centerTextHOffset)\n" + encoded += "verticalTextOffset: \(verticalTextOffset)\n" + encoded += "horizontalTextOffset: \(horizontalTextOffset)\n" + encoded += "watchWidth: \(watchSize.width)\n" + encoded += "watchHeight: \(watchSize.height)\n" + encoded += "cornerRadiusRatio: \(cornerRadiusRatio)\n" + } + return encoded + } + + func extract(from str: String) -> [String: String] { + let regex = /^([a-zA-Z_0-9]+)\s*:[\s"]*([^\s"#][^"#]*)[\s"#]*(#*.*)$/ + var values = [String: String]() + for line in str.split(whereSeparator: \.isNewline) { + if let match = try? regex.firstMatch(in: String(line))?.output { + values[String(match.1)] = String(match.2) + } + } + return values + } + + func update(from values: [String: String]) { + let seperatorRegex = /(\s*;|\{\})/ + func readGradient(value: String?) -> Gradient? { + guard let value = value else { return nil } + let newValue = value.replacing(seperatorRegex) { _ in "\n" } + return Gradient(from: newValue) + } + + func readColorList(_ list: String?) -> [CGColor]? { + var colors = [CGColor?]() + if let colorValues = list { + for color in colorValues.split(separator: ",") { + colors.append(String(color).colorValue) + } + return colors.flattened() + } else { + return nil + } + } + + globalMonth = values["globalMonth"]?.boolValue ?? globalMonth + apparentTime = values["apparentTime"]?.boolValue ?? apparentTime + locationEnabled = values["locationEnabled"]?.boolValue ?? locationEnabled + location = CGPoint(from: values["customLocation"]) + firstRing = readGradient(value: values["firstRing"]) ?? firstRing + secondRing = readGradient(value: values["secondRing"]) ?? secondRing + thirdRing = readGradient(value: values["thirdRing"]) ?? thirdRing + innerColor = values["innerColor"]?.colorValue ?? innerColor + majorTickColor = values["majorTickColor"]?.colorValue ?? majorTickColor + majorTickAlpha = values["majorTickAlpha"]?.floatValue ?? majorTickAlpha + minorTickColor = values["minorTickColor"]?.colorValue ?? minorTickColor + minorTickAlpha = values["minorTickAlpha"]?.floatValue ?? minorTickAlpha + fontColor = values["fontColor"]?.colorValue ?? fontColor + centerFontColor = readGradient(value: values["centerFontColor"]) ?? centerFontColor + evenSolarTermTickColor = values["evenSolarTermTickColor"]?.colorValue ?? evenSolarTermTickColor + oddSolarTermTickColor = values["oddSolarTermTickColor"]?.colorValue ?? oddSolarTermTickColor + innerColorDark = values["innerColorDark"]?.colorValue ?? innerColor + majorTickColorDark = values["majorTickColorDark"]?.colorValue ?? majorTickColor + minorTickColorDark = values["minorTickColorDark"]?.colorValue ?? minorTickColor + fontColorDark = values["fontColorDark"]?.colorValue ?? fontColor + evenSolarTermTickColorDark = values["evenSolarTermTickColorDark"]?.colorValue ?? evenSolarTermTickColorDark + oddSolarTermTickColorDark = values["oddSolarTermTickColorDark"]?.colorValue ?? oddSolarTermTickColorDark + if let colourList = readColorList(values["planetIndicator"]), colourList.count == self.planetIndicator.count { + planetIndicator = colourList + } + if let colourList = readColorList(values["sunPositionIndicator"]), colourList.count == self.sunPositionIndicator.count { + sunPositionIndicator = colourList + } + if let colourList = readColorList(values["moonPositionIndicator"]), colourList.count == self.moonPositionIndicator.count { + moonPositionIndicator = colourList + } + eclipseIndicator = values["eclipseIndicator"]?.colorValue ?? eclipseIndicator + fullmoonIndicator = values["fullmoonIndicator"]?.colorValue ?? fullmoonIndicator + oddStermIndicator = values["oddStermIndicator"]?.colorValue ?? oddStermIndicator + evenStermIndicator = values["evenStermIndicator"]?.colorValue ?? evenStermIndicator + shadeAlpha = values["shadeAlpha"]?.floatValue ?? shadeAlpha + shadowSize = values["shadowSize"]?.floatValue ?? shadowSize + centerTextOffset = values["centerTextOffset"]?.floatValue ?? centerTextOffset + centerTextHOffset = values["centerTextHorizontalOffset"]?.floatValue ?? centerTextHOffset + verticalTextOffset = values["verticalTextOffset"]?.floatValue ?? verticalTextOffset + horizontalTextOffset = values["horizontalTextOffset"]?.floatValue ?? horizontalTextOffset + if let width = values["watchWidth"]?.floatValue, let height = values["watchHeight"]?.floatValue { + watchSize = CGSize(width: width, height: height) + } + cornerRadiusRatio = values["cornerRadiusRatio"]?.floatValue ?? cornerRadiusRatio + } + + func update(from str: String) { + let values = extract(from: str) + update(from: values) + } + + func loadStatic() { + let filePath = Bundle.main.path(forResource: "layout", ofType: "txt")! + let defaultLayout = try! String(contentsOfFile: filePath) + self.update(from: defaultLayout) + } + + func loadDefault(context: ModelContext, local: Bool = false) { + let defaultName = ThemeData.defaultName + let predicate = { + let deviceName = if local { + try? LocalData.read()?.deviceName ?? ThemeData.deviceName + } else { + ThemeData.deviceName + } + return #Predicate { data in + data.name == defaultName && data.deviceName == deviceName + } + }() + let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + + do { + let themes = try context.fetch(descriptor) + var found = false + for theme in themes { + if !found && !theme.isNil { + self.update(from: theme.code!) + found = true + break + } + } + if !found { + let filePath = Bundle.main.path(forResource: "layout", ofType: "txt")! + let defaultLayout = try! String(contentsOfFile: filePath) + self.update(from: defaultLayout) + } + } catch { + fatalError(error.localizedDescription) + } + } + + func saveDefault(context: ModelContext) { + let defaultName = ThemeData.defaultName + let deviceName = ThemeData.deviceName + let predicate = #Predicate { data in + data.name == defaultName && data.deviceName == deviceName + } + let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + do { + try LocalData.write(deviceName: deviceName) + let themes = try context.fetch(descriptor) + var found = false + for theme in themes { + if !found && !theme.isNil { + theme.update(code: self.encode()) + found = true + } else { + context.delete(theme) + } + } + if !found { + let defaultTheme = ThemeData(name: ThemeData.defaultName, code: self.encode()) + context.insert(defaultTheme) + } + } catch { + fatalError(error.localizedDescription) + } + } +} diff --git a/Shared/Model.swift b/Shared/DataModel/Model.swift similarity index 85% rename from Shared/Model.swift rename to Shared/DataModel/Model.swift index b186be4..06233ff 100644 --- a/Shared/Model.swift +++ b/Shared/DataModel/Model.swift @@ -6,16 +6,29 @@ // import Foundation +import Observation infix operator %% -extension Int { - static func %%(_ left: Int, _ right: Int) -> Int { +protocol NamedPoint { + var name: String { get } + var pos: Double { get } +} + +extension BinaryInteger { + static func %%(_ left: Self, _ right: Self) -> Self { let mod = left % right return mod >= 0 ? mod : mod + right } } +extension FloatingPoint { + static func %%(_ left: Self, _ right: Self) -> Self { + let mod = left.truncatingRemainder(dividingBy: right) + return mod >= 0 ? mod : mod + right + } +} + func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } @@ -431,7 +444,8 @@ extension Array { } } -final class ChineseCalendar { +@Observable final class ChineseCalendar { + static let updateInterval: CGFloat = 14.4 //Seconds static let month_chinese = ["冬月", "臘月", "正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月"] static let month_chinese_compact = ["㋊", "㋋", "㋀", "㋁", "㋂", "㋃", "㋄", "㋅", "㋆", "㋇", "㋈", "㋉"] static let day_chinese = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"] @@ -445,35 +459,52 @@ final class ChineseCalendar { static let alternativeMonthName = ["閏正月": "閏一月"] static let dayTimeName = ["夜中", "日出", "日中", "日入"] static let moonTimeName = ["月出", "月中", "月入"] - static let MoonPhases = ["朔", "望"] - static var globalMonth = false - static var apparentTime = false - - private let _compact: Bool - private var _time: Date - private var _calendar: Calendar - private var _location: CGPoint? - private lazy var _year: Int = 0 - private lazy var _year_length: Double = 0 - private lazy var _solarTerms: [Date] = [] - private lazy var _evenSolarTerms: [Date] = [] - private lazy var _oddSolarTerms: [Date] = [] - private lazy var _moonEclipses: [Date] = [] - private lazy var _fullMoons: [Date] = [] - private lazy var _monthNames: [String] = [] - private lazy var _monthNamesFull: [String] = [] - private lazy var _month: Int = -1 - private lazy var _precise_month: Int = -1 - private lazy var _day: Int = -1 - private lazy var _sunTimes: [Date?] = [] - private lazy var _moonTimes: [Date?] = [] - private lazy var _startHour: Date = .distantPast - private lazy var _endHour: Date = .distantFuture - private lazy var _hourNames: [String] = [] - private lazy var _hour_string: String = "" - private lazy var _quarter_string: String = "" - - struct NamedPosition { + static let moonPhases = ["朔", "望"] + static let holidays = ["正月一日": "元旦", "正月十五": "上元", "三月三日": "上巳", "五月五日": "端午", "七月七日": "七夕", "七月十五": "中元", "九月九日": "重陽", "八月十五": "中秋", "冬月十五": "下元", "大年三十": "除夕"] + static let start: Date = { + var components = DateComponents() + components.year = 1901 + components.month = 12 + components.day = 23 + components.timeZone = TimeZone.gmt + return Calendar.utcCalendar.date(from: components)! + }() + static let end: Date = { + var components = DateComponents() + components.year = 2999 + components.month = 12 + components.day = 20 + components.timeZone = TimeZone.gmt + return Calendar.utcCalendar.date(from: components)! + }() + + @ObservationIgnored private let _compact: Bool + private var _globalMonth: Bool = false + private var _apparentTime: Bool = false + private var _time: Date = .distantPast + private var _calendar: Calendar = .utcCalendar + private var _location: CGPoint? = nil + @ObservationIgnored private var _year: Int = 0 + @ObservationIgnored private var _year_length: Double = 0 + @ObservationIgnored private var _solarTerms: [Date] = [] + @ObservationIgnored private var _evenSolarTerms: [Date] = [] + @ObservationIgnored private var _oddSolarTerms: [Date] = [] + @ObservationIgnored private var _moonEclipses: [Date] = [] + @ObservationIgnored private var _fullMoons: [Date] = [] + @ObservationIgnored private var _monthNames: [String] = [] + @ObservationIgnored private var _monthNamesFull: [String] = [] + @ObservationIgnored private var _month: Int = -1 + @ObservationIgnored private var _precise_month: Int = -1 + @ObservationIgnored private var _day: Int = -1 + @ObservationIgnored private var _sunTimes: [Date?] = [] + @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 _hour_string: String = "" + @ObservationIgnored private var _quarter_string: String = "" + + struct NamedPosition: NamedPoint { let name: String let pos: Double } @@ -496,8 +527,8 @@ final class ChineseCalendar { } struct Ticks { - struct TickName { - var position: Double = 0.0 + struct TickName: NamedPoint { + var pos: Double = 0.0 var name: String = "" var active: Bool = false } @@ -507,23 +538,27 @@ final class ChineseCalendar { var minorTicks = [Double]() } - init(time: Date, timezone: TimeZone, location: CGPoint?, compact: Bool = false) { + init(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint? = nil, compact: Bool = false) { + self._compact = compact self._time = time - var calendar = Calendar(identifier: .iso8601) - calendar.timeZone = timezone + var calendar = Calendar.current + calendar.timeZone = timezone ?? calendar.timeZone self._calendar = calendar - self._location = location - self._compact = compact + self._location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location + self._globalMonth = WatchLayout.shared.globalMonth + self._apparentTime = WatchLayout.shared.apparentTime updateYear() updateDate() updateHour() } - - private init(compact: Bool, time: Date, calendar: Calendar, location: CGPoint?, year: Int, year_length: Double, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], monthNames: [String], monthNamesFull: [String], month: Int, precise_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, year: Int, year_length: Double, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], monthNames: [String], monthNamesFull: [String], month: Int, precise_month: Int, day: Int, sunTimes: [Date?], moonTimes: [Date?], startHour: Date, endHour: Date, hourNames: [String], 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._year = year self._year_length = year_length self._solarTerms = solarTerms @@ -546,7 +581,7 @@ final class ChineseCalendar { } var copy: ChineseCalendar { - ChineseCalendar(compact: _compact, time: _time, calendar: _calendar, location: _location, year: _year, year_length: _year_length, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, monthNames: _monthNames, monthNamesFull: _monthNamesFull, month: _month, precise_month: _precise_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, year: _year, year_length: _year_length, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, monthNames: _monthNames, monthNamesFull: _monthNamesFull, month: _month, precise_month: _precise_month, day: _day, sunTimes: _sunTimes, moonTimes: _moonTimes, startHour: _startHour, endHour: _endHour, hourNames: _hourNames, hour_string: _hour_string, quarter_string: _quarter_string) } private func updateYear() { @@ -571,10 +606,10 @@ final class ChineseCalendar { var start: Int?, end: Int? for i in 0..= solar_terms[0] { start = i - 1 @@ -595,12 +630,12 @@ final class ChineseCalendar { while i + 1 < eclipse.count, j < evenSolarTerms.count { let thisEclipse: Date let nextEclipse: Date - if Self.globalMonth { + if _globalMonth { thisEclipse = eclipse[i] nextEclipse = eclipse[i + 1] } else { - thisEclipse = calendar.startOfDay(for: eclipse[i], apparent: Self.apparentTime, location: location) - nextEclipse = calendar.startOfDay(for: eclipse[i + 1], apparent: Self.apparentTime, location: location) + thisEclipse = calendar.startOfDay(for: eclipse[i], apparent: apparentTime, location: location) + nextEclipse = calendar.startOfDay(for: eclipse[i + 1], apparent: apparentTime, location: location) } if thisEclipse <= evenSolarTerms[j], nextEclipse > evenSolarTerms[j] { count += 1 @@ -658,8 +693,8 @@ final class ChineseCalendar { private func updateDate() { var i = 0 while i < _moonEclipses.count - 1 { - let startOfEclipse = _calendar.startOfDay(for: _moonEclipses[i], apparent: apparentTime, location: _location) - let startOfDate = _calendar.startOfDay(for: _time, apparent: apparentTime, location: _location) + let startOfEclipse = _calendar.startOfDay(for: _moonEclipses[i], apparent: apparentTime, location: location) + let startOfDate = _calendar.startOfDay(for: _time, apparent: apparentTime, location: location) if startOfEclipse > startOfDate { break } @@ -672,8 +707,8 @@ final class ChineseCalendar { } j += 1 } - let previousEclipse = _calendar.startOfDay(for: _moonEclipses[i - 1], apparent: apparentTime, location: _location) - let startOfDate = _calendar.startOfDay(for: _time, apparent: apparentTime, location: _location) + let previousEclipse = _calendar.startOfDay(for: _moonEclipses[i - 1], apparent: apparentTime, location: location) + let startOfDate = _calendar.startOfDay(for: _time, apparent: apparentTime, location: location) let date_diff = Int(round(previousEclipse.distance(to: startOfDate) / 86400)) _month = i - 1 _precise_month = j - 1 @@ -687,7 +722,7 @@ final class ChineseCalendar { var tempEndHour: Date? var hour = startOfDay _hourNames = [] - while hour < startOfNextDay - 0.01 { + while hour < startOfNextDay - 1 { let hourIndex: Int if apparentTime { hourIndex = Int(round(startOfDay.distance(to: hour) / startOfDay.distance(to: startOfNextDay) * 24)) @@ -736,31 +771,36 @@ final class ChineseCalendar { _endHour = tempEndHour! } - func update(time: Date, timezone: TimeZone, location: CGPoint?) { + func update(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint? = nil) { + let oldTimezone = _calendar.timeZone + let oldLocation = _location + let oldGlobalMonth = _globalMonth + let oldApparentTime = _apparentTime _time = time - _calendar.timeZone = timezone - _location = location - - if (location == nil && _location != nil) || (location != nil && _location == nil) { + _calendar.timeZone = timezone ?? _calendar.timeZone + _location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location + _globalMonth = WatchLayout.shared.globalMonth + _apparentTime = WatchLayout.shared.apparentTime + + if (location == nil && oldLocation != nil) || (location != nil && oldLocation == nil) { updateYear() - } else if let newLocation = location, let oldLocation = _location { - if sqrt(pow(newLocation.x - oldLocation.x, 2) + pow(newLocation.y - oldLocation.y, 2)) > 1 { - updateYear() - } - } else if timezone != _calendar.timeZone { + } else if let newLocation = location, let oldLocation = oldLocation, + sqrt(pow(newLocation.x - oldLocation.x, 2) + pow(newLocation.y - oldLocation.y, 2)) > 1 { updateYear() - } - - let year = _calendar.component(.year, from: time) - if !(((year == _year) && (_solarTerms[24] > time)) || ((year == _year - 1) && (_solarTerms[0] <= time))) { + } else if timezone != oldTimezone || oldGlobalMonth != _globalMonth || oldApparentTime != _apparentTime { updateYear() + } else { + let year = _calendar.component(.year, from: time) + if !(((year == _year) && (_solarTerms[24] > time)) || ((year == _year - 1) && (_solarTerms[0] <= time))) { + updateYear() + } } updateDate() updateHour() } var apparentTime: Bool { - Self.apparentTime && _location != nil + _apparentTime && _location != nil } var monthString: String { @@ -842,10 +882,10 @@ final class ChineseCalendar { var phases = [NamedDate]() for i in 0.. 0 && $0 < 1 } var previousMonthDivide = 0.0 @@ -867,7 +907,7 @@ final class ChineseCalendar { let position = (monthDivides[i] + previousMonthDivide) / 2 if position - previousMonthDivide > minMonthLength * Double(_monthNames[i].count) { monthNames.append(Ticks.TickName( - position: position, + pos: position, name: _monthNames[i], active: previousMonthDivide <= currentDayInYear )) @@ -877,7 +917,7 @@ final class ChineseCalendar { let position = (1 + previousMonthDivide) / 2 if position - previousMonthDivide > minMonthLength * Double(_monthNames[(monthDivides.count) %% _monthNames.count].count) { monthNames.append(Ticks.TickName( - position: position, + pos: position, name: _monthNames[(monthDivides.count) %% _monthNames.count], active: previousMonthDivide <= currentDayInYear )) @@ -895,13 +935,13 @@ final class ChineseCalendar { let monthStart: Date let monthEnd: Date var date: Date - if Self.globalMonth { + if _globalMonth { monthStart = _moonEclipses[_precise_month] monthEnd = _moonEclipses[_precise_month + 1] - date = _calendar.startOfDay(for: monthStart, apparent: Self.apparentTime, location: _location) + date = _calendar.startOfDay(for: monthStart, apparent: apparentTime, location: location) } else { - monthStart = _calendar.startOfDay(for: _moonEclipses[_month], apparent: Self.apparentTime, location: _location) - monthEnd = _calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: Self.apparentTime, location: _location) + monthStart = _calendar.startOfDay(for: _moonEclipses[_month], apparent: apparentTime, location: location) + monthEnd = _calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: apparentTime, location: location) date = monthStart } @@ -909,7 +949,7 @@ final class ChineseCalendar { while date < monthEnd { if apparentTime { date += 86400 * 1.5 - date = _calendar.startOfDay(for: date, apparent: Self.apparentTime, location: _location) + date = _calendar.startOfDay(for: date, apparent: apparentTime, location: location) } else { date = _calendar.date(byAdding: .day, value: 1, to: date)! } @@ -931,7 +971,7 @@ final class ChineseCalendar { let position = (majorTicks[i] + previousDayDivide) / 2 if position - previousDayDivide > minDayLength * Double(allDayNames[i].count) { dayNames.append(Ticks.TickName( - position: position, + pos: position, name: allDayNames[i], active: previousDayDivide <= currentDayInMonth )) @@ -941,7 +981,7 @@ final class ChineseCalendar { let position = (1 + previousDayDivide) / 2 if position - previousDayDivide > minDayLength * Double(allDayNames[majorTicks.count].count) { dayNames.append(Ticks.TickName( - position: position, + pos: position, name: allDayNames[majorTicks.count], active: previousDayDivide <= currentDayInMonth )) @@ -975,7 +1015,7 @@ final class ChineseCalendar { for i in 0.. minimumSubhourLength { subHourNames.append(Ticks.TickName( - position: subHourTick[i], + pos: subHourTick[i], name: Self.chinese_numbers[count], active: subHourTick[i] <= subhourInHour )) @@ -1078,12 +1118,52 @@ final class ChineseCalendar { ticks.minorTicks = subQuarterTick return ticks } + + func nextHours(count: Int) -> [Date] { + var tickTime = startOfDay + var i = 0 + var hours = [Date]() + while i < count { + if tickTime > time { + hours.append(tickTime) + i += 1 + } + if apparentTime { + tickTime += startOfDay.distance(to: startOfNextDay) / 24 + } else { + tickTime = _calendar.date(byAdding: .hour, value: 1, to: tickTime)! + } + } + return hours + } + + func nextQuarters(count: Int) -> [Date] { + var tickTime = startOfDay - 864 * 6 + var i = 0 + var quarters = [Date]() + let hours = nextHours(count: count / 4 + 1) + var j = 0 + while i < count && j < hours.count { + if tickTime > time { + if tickTime > hours[j] { + if tickTime > hours[j] + 16 { + quarters.append(hours[j]) + } + j += 1 + } + if j >= hours.count || tickTime < hours[j] - 16 { + quarters.append(tickTime) + i += 1 + } + } + tickTime += 864 + } + return quarters + } var location: CGPoint? { get { _location - } set { - _location = newValue } } @@ -1100,7 +1180,7 @@ final class ChineseCalendar { } var preciseMonth: Int { - Self.globalMonth ? _precise_month : _month + _globalMonth ? _precise_month : _month } var day: Int { @@ -1112,9 +1192,9 @@ final class ChineseCalendar { } var monthLengthInWholeDays: Int { - let month = Self.globalMonth ? _precise_month : _month - let monthStartDate = _calendar.startOfDay(for: _moonEclipses[month], apparent: Self.apparentTime, location: _location) - let monthEndDate = _calendar.startOfDay(for: _moonEclipses[month + 1], apparent: Self.apparentTime, location: _location) + let month = _globalMonth ? _precise_month : _month + let monthStartDate = _calendar.startOfDay(for: _moonEclipses[month], apparent: apparentTime, location: location) + let monthEndDate = _calendar.startOfDay(for: _moonEclipses[month + 1], apparent: apparentTime, location: location) return Int(round(monthStartDate.distance(to: monthEndDate) / 86400)) } @@ -1149,12 +1229,12 @@ final class ChineseCalendar { } var currentDayInMonth: Double { - if Self.globalMonth { + if _globalMonth { let monthLength = _moonEclipses[_precise_month].distance(to: _moonEclipses[_precise_month + 1]) return _moonEclipses[_precise_month].distance(to: _time) / monthLength } else { - let monthStart = calendar.startOfDay(for: _moonEclipses[_month], apparent: Self.apparentTime, location: _location) - let monthEnd = calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: Self.apparentTime, location: _location) + let monthStart = calendar.startOfDay(for: _moonEclipses[_month], apparent: apparentTime, location: location) + let monthEnd = calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: apparentTime, location: location) return monthStart.distance(to: _time) / monthStart.distance(to: monthEnd) } } @@ -1168,12 +1248,12 @@ final class ChineseCalendar { } var startOfDay: Date { - _calendar.startOfDay(for: _time, apparent: Self.apparentTime, location: _location) + _calendar.startOfDay(for: _time, apparent: apparentTime, location: location) } var startOfNextDay: Date { let nextDay = startOfDay + 86400 * 1.5 - return _calendar.startOfDay(for: nextDay, apparent: Self.apparentTime, location: _location) + return _calendar.startOfDay(for: nextDay, apparent: apparentTime, location: location) } var planetPosition: [NamedPosition] { @@ -1186,22 +1266,22 @@ final class ChineseCalendar { var eventInMonth: CelestialEvent { let monthStart: Date let monthLength: Double - if Self.globalMonth { + if _globalMonth { monthStart = _moonEclipses[_precise_month] monthLength = _moonEclipses[_precise_month].distance(to: _moonEclipses[_precise_month + 1]) } else { - monthStart = _calendar.startOfDay(for: _moonEclipses[_month], apparent: Self.apparentTime, location: _location) - let monthEnd = _calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: Self.apparentTime, location: _location) + monthStart = _calendar.startOfDay(for: _moonEclipses[_month], apparent: apparentTime, location: location) + let monthEnd = _calendar.startOfDay(for: _moonEclipses[_month + 1], apparent: apparentTime, location: location) monthLength = monthStart.distance(to: monthEnd) } var event = CelestialEvent() - if !Self.globalMonth { + if !_globalMonth { event.eclipse = _moonEclipses.map { date in - NamedPosition(name: Self.MoonPhases[0], pos: monthStart.distance(to: date) / monthLength) + NamedPosition(name: Self.moonPhases[0], pos: monthStart.distance(to: date) / monthLength) }.filter { $0.pos >= 0 && $0.pos < 1 } } event.fullMoon = _fullMoons.map { date in - NamedPosition(name: Self.MoonPhases[1], pos: monthStart.distance(to: date) / monthLength) + NamedPosition(name: Self.moonPhases[1], pos: monthStart.distance(to: date) / monthLength) }.filter { $0.pos >= 0 && $0.pos < 1 } event.evenSolarTerm = _evenSolarTerms.enumerated().map { offset, date in let name = String(Self.evenSolarTermChinese[(offset - 1) %% Self.evenSolarTermChinese.count].replacingOccurrences(of: " ", with: "")) @@ -1219,10 +1299,10 @@ final class ChineseCalendar { let lengthOfDay = startOfDay.distance(to: startOfNextDay) var event = CelestialEvent() event.eclipse = _moonEclipses.map { date in - NamedPosition(name: Self.MoonPhases[0], pos: startOfDay.distance(to: date) / lengthOfDay) + NamedPosition(name: Self.moonPhases[0], pos: startOfDay.distance(to: date) / lengthOfDay) }.filter { $0.pos >= 0 && $0.pos < 1 } event.fullMoon = _fullMoons.map { date in - NamedPosition(name: Self.MoonPhases[1], pos: startOfDay.distance(to: date) / lengthOfDay) + NamedPosition(name: Self.moonPhases[1], pos: startOfDay.distance(to: date) / lengthOfDay) }.filter { $0.pos >= 0 && $0.pos < 1 } event.evenSolarTerm = _evenSolarTerms.enumerated().map { offset, date in let name = String(Self.evenSolarTermChinese[(offset - 1) %% Self.evenSolarTermChinese.count].replacingOccurrences(of: " ", with: "")) @@ -1239,10 +1319,10 @@ final class ChineseCalendar { var event = CelestialEvent() let hourLength = startHour.distance(to: endHour) event.eclipse = _moonEclipses.map { date in - NamedPosition(name: Self.MoonPhases[0], pos: startHour.distance(to: date) / hourLength) + NamedPosition(name: Self.moonPhases[0], pos: startHour.distance(to: date) / hourLength) }.filter { $0.pos >= 0 && $0.pos < 1 } event.fullMoon = _fullMoons.map { date in - NamedPosition(name: Self.MoonPhases[1], pos: startHour.distance(to: date) / hourLength) + NamedPosition(name: Self.moonPhases[1], pos: startHour.distance(to: date) / hourLength) }.filter { $0.pos >= 0 && $0.pos < 1 } event.evenSolarTerm = _evenSolarTerms.enumerated().map { offset, date in let name = String(Self.evenSolarTermChinese[(offset - 1) %% Self.evenSolarTermChinese.count].replacingOccurrences(of: " ", with: "")) @@ -1269,7 +1349,7 @@ final class ChineseCalendar { let chineseCalendar = copy let startOfDay = startOfDay let startOfNextDay = startOfNextDay - if let location = _location { + if let location = location { let today = datesToNamedDates(dates: intraday_solar_times(chineseCalendar: chineseCalendar, location: location), names: Self.dayTimeName) chineseCalendar.update(time: startOfDay - 12 * 3600, timezone: _calendar.timeZone, location: location) let previousDay = datesToNamedDates(dates: intraday_solar_times(chineseCalendar: chineseCalendar, location: location), names: Self.dayTimeName) @@ -1288,7 +1368,7 @@ final class ChineseCalendar { let chineseCalendar = copy let startOfDay = startOfDay let startOfNextDay = startOfNextDay - if let location = _location { + if let location = location { let today = datesToNamedDates(dates: intraday_lunar_times(chineseCalendar: chineseCalendar, location: location), names: Self.moonTimeName) chineseCalendar.update(time: startOfDay - 12 * 3600, timezone: _calendar.timeZone, location: location) let previousDay = datesToNamedDates(dates: intraday_lunar_times(chineseCalendar: chineseCalendar, location: location), names: Self.moonTimeName) @@ -1305,7 +1385,7 @@ final class ChineseCalendar { var sunMoonPositions: DailyEvent { var dailyEvent = DailyEvent() - if let location = _location { + if let location = location { _sunTimes = intraday_solar_times(chineseCalendar: self, location: location) _moonTimes = intraday_lunar_times(chineseCalendar: self, location: location) let startOfDay = startOfDay @@ -1350,7 +1430,7 @@ final class ChineseCalendar { return nil } } - if let location = _location { + if let location = location { if _sunTimes.count == 0 || _moonTimes.count == 0 { _sunTimes = intraday_solar_times(chineseCalendar: self, location: location) _moonTimes = intraday_lunar_times(chineseCalendar: self, location: location) @@ -1377,4 +1457,38 @@ final class ChineseCalendar { } return dailyEvent } + + var lunarHoliday: String? { + if let holiday = Self.holidays[self.dateString] { + return holiday + } else { + let nextDay = self.copy + nextDay.update(time: self.startOfNextDay + 1) + if nextDay.dateString == "正月一日" { + return Self.holidays["大年三十"] + } else { + return nil + } + } + } + + var holidays: [String] { + var holidays: [String] = [] + if let holiday = self.lunarHoliday { + holidays.append(holiday) + } + for solarTerm in self.eventInDay.oddSolarTerm { + holidays.append(solarTerm.name) + } + for solarTerm in self.eventInDay.evenSolarTerm { + holidays.append(solarTerm.name) + } + for moon in self.eventInDay.eclipse { + holidays.append(moon.name) + } + for moon in self.eventInDay.fullMoon { + holidays.append(moon.name) + } + return holidays + } } diff --git a/Shared/PlanetModel.swift b/Shared/DataModel/PlanetModel.swift similarity index 100% rename from Shared/PlanetModel.swift rename to Shared/DataModel/PlanetModel.swift diff --git a/Shared/DataModel/ThemeData.swift b/Shared/DataModel/ThemeData.swift new file mode 100644 index 0000000..94dec8e --- /dev/null +++ b/Shared/DataModel/ThemeData.swift @@ -0,0 +1,234 @@ +// +// Layout.swift +// Chinese Time +// +// Created by Leo Liu on 7/7/23. +// +// + +import Foundation +import SwiftData +#if os(macOS) +import SystemConfiguration +#elseif os(iOS) +import UIKit +#elseif os(watchOS) +import WatchKit +#endif + +typealias ThemeData = DataSchemaV2.Layout + +extension ThemeData: Identifiable, Hashable { + static var version: Int { + intVersion(DataSchemaV2.versionIdentifier) + } + +#if os(macOS) + static let groupId = Bundle.main.object(forInfoDictionaryKey: "GroupID") as! String +#elseif os(iOS) + static let groupId = "group.ChineseTime" +#elseif os(watchOS) + static let groupId = "group.ChineseTime.Watch" +#endif + +#if os(macOS) + static let deviceName = SCDynamicStoreCopyComputerName(nil, nil).map { String($0) } ?? "Mac" +#elseif os(iOS) + @MainActor static let deviceName = UIDevice.current.name +#elseif os(watchOS) + static let deviceName = WKInterfaceDevice.current().name +#endif + + static let defaultName = NSLocalizedString("Default", comment: "Default save file name") + + static let container = { + let fullSchema = Schema(versionedSchema: DataSchemaV2.self) + let baseUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ThemeData.groupId)! +#if os(macOS) + let url = baseUrl.appendingPathComponent("ChineseTime") +#else + let url = baseUrl.appendingPathComponent("ChineseTime.sqlite") +#endif + let modelConfig = ModelConfiguration("ChineseTime", schema: fullSchema, url: url, cloudKitDatabase: .private("iCloud.YLiu.ChineseTime")) + return createContainer(schema: fullSchema, migrationPlan: DataMigrationPlan.self, configurations: [modelConfig]) + }() + + static let context = ModelContext(ThemeData.container) + + static func latestVersion() -> Int { + let deviceName = ThemeData.deviceName + let predicate = #Predicate { data in + data.deviceName == deviceName + } + 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) + } + return version + } + + var isNil: Bool { + return code == nil || name == nil || deviceName == nil || modifiedDate == nil + } + + func update(code: String) { + if self.code != code { + self.code = code + self.modifiedDate = Date.now + self.version = ThemeData.version + } + } +} + +private func intVersion(_ version: Schema.Version) -> Int { + version.major * 100_0000 + version.minor * 1_0000 + version.patch * 100 + 0 +} + +private func createContainer(schema: Schema, migrationPlan: SchemaMigrationPlan.Type? = nil, configurations: [ModelConfiguration]) -> ModelContainer { + do { + return try ModelContainer(for: schema, migrationPlan: migrationPlan, configurations: configurations) + } catch { + print(error.localizedDescription) + do { + return try ModelContainer(for: schema, configurations: configurations) + } catch { + fatalError(error.localizedDescription) + } + } +} + +enum DataSchemaV2: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(1, 1, 0) + static var models: [any PersistentModel.Type] { + [Layout.self] + } + + @Model final class Layout { + var code: String? + var deviceName: String? + var modifiedDate: Date? + var name: String? + var version: Int? + + init(name: String, code: String) { + self.name = name + self.deviceName = Layout.deviceName + self.code = code + self.modifiedDate = Date.now + self.version = intVersion(DataSchemaV2.versionIdentifier) + } + } +} + +enum DataSchemaV1: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(1, 0, 0) + static var models: [any PersistentModel.Type] { + [Layout.self] + } + + @Model final class Layout { + var code: String? + var deviceName: String? + var modifiedDate: Date? + var name: String? + + init(name: String, deviceName: String, code: String) { + self.name = name + self.deviceName = deviceName + self.code = code + self.modifiedDate = Date.now + } + } +} + +enum DataMigrationPlan: SchemaMigrationPlan { + static var schemas: [any VersionedSchema.Type] { + [DataSchemaV1.self, DataSchemaV2.self] + } + + static var stages: [MigrationStage] { [migrateV1toV2] } + + static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self) +} + +enum LocalSchemaV1: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(1, 1, 0) + static var models: [any PersistentModel.Type] { + [LocalData.self] + } + + @Model final class LocalData { + var deviceName: String? + var modifiedDate: Date? + var version: Int? + + init(deviceName: String) { + self.deviceName = deviceName + self.modifiedDate = Date.now + self.version = intVersion(LocalSchemaV1.versionIdentifier) + } + } +} + +typealias LocalData = LocalSchemaV1.LocalData + +extension LocalData: Identifiable, Hashable { + static var version: Int { + intVersion(LocalSchemaV1.versionIdentifier) + } + + static let container = { + let localSchema = Schema(versionedSchema: LocalSchemaV1.self) + let modelConfig = ModelConfiguration("ChineseTimeLocal", schema: localSchema, groupContainer: .identifier(ThemeData.groupId), cloudKitDatabase: .none) + return createContainer(schema: localSchema, migrationPlan: nil, configurations: [modelConfig]) + }() + + static let context = ModelContext(LocalData.container) + + func update(deviceName: String) { + if self.deviceName != deviceName { + self.deviceName = deviceName + self.modifiedDate = Date.now + self.version = LocalData.version + } + } + + static func read() throws -> LocalData? { + let context = LocalData.context + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\LocalData.modifiedDate, order: .reverse)]) + let records = try context.fetch(descriptor) + for record in records { + return record + } + return nil + } + + static func write(deviceName: String) throws { + let context = LocalData.context + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\LocalData.modifiedDate, order: .reverse)]) + let records = try context.fetch(descriptor) + var found = false + for record in records { + if !found { + record.update(deviceName: deviceName) + found = true + } else { + context.delete(record) + } + } + if !found { + let record = LocalData(deviceName: deviceName) + context.insert(record) + } + try context.save() + } +} diff --git a/Shared/Delegates.swift b/Shared/Delegates.swift deleted file mode 100644 index ffbdfb2..0000000 --- a/Shared/Delegates.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// Delegates.swift -// MacWidgetExtension -// -// Created by Leo Liu on 5/9/23. -// - -import CoreData -import CoreLocation -#if os(macOS) -import SystemConfiguration -#elseif os(iOS) -import UIKit -#elseif os(watchOS) -import WatchKit -#endif - -final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { - static let shared = LocationManager() - - @Published private var _location: CGPoint? - var location: CGPoint? { - get { - _location - } set { - _location = newValue - if newValue == nil { - lastUpdated = Date.distantPast - } else { - lastUpdated = Date() - } - } - } - - let manager = CLLocationManager() - private var completion: ((CGPoint?) -> Void)? - private var lastUpdated = Date.distantPast - var enabled = true - - override init() { - super.init() - manager.delegate = self - manager.requestWhenInUseAuthorization() - manager.desiredAccuracy = kCLLocationAccuracyKilometer - } - - func requestLocation(completion: ((CGPoint?) -> Void)?) { - if let completion = completion { - self.completion = completion - } - if enabled && (lastUpdated.distance(to: Date()) > 3600) { - switch manager.authorizationStatus { -#if os(macOS) - case .authorized, .authorizedAlways: - manager.startUpdatingLocation() -#else - case .authorizedWhenInUse, .authorizedAlways: - manager.startUpdatingLocation() -#endif - case .notDetermined: // Authorization not determined yet. - manager.requestWhenInUseAuthorization() - default: - if let completion = completion { - completion(nil) - self.completion = nil - } - } - } - } - - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.last { - manager.stopUpdatingLocation() - self.location = CGPoint(x: location.coordinate.latitude, y: location.coordinate.longitude) - if let completion = completion { - completion(self.location) - self.completion = nil - } - } - } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - switch manager.authorizationStatus { -#if os(macOS) - case .authorized, .authorizedAlways: // Location services are available. - requestLocation(completion: nil) -#else - case .authorizedWhenInUse, .authorizedAlways: // Location services are available. - requestLocation(completion: nil) -#endif - - case .restricted, .denied: - break - - case .notDetermined: // Authorization not determined yet. - manager.requestWhenInUseAuthorization() - - @unknown default: - print("Unhandled Location Authorization Case") - } - } - - func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - print(error) - } -} - -final class DataContainer: ObservableObject { - static let shared = DataContainer() - -#if os(macOS) - static let groupId = Bundle.main.object(forInfoDictionaryKey: "GroupID") as! String -#elseif os(iOS) - static let groupId = "group.ChineseTime" -#elseif os(watchOS) - static let groupId = "group.ChineseTime.Watch" -#endif - - lazy var persistentContainer: NSPersistentCloudKitContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentCloudKitContainer(name: "ChineseTime") -#if DEBUG - do { - // Use the container to initialize the development schema. - try container.initializeCloudKitSchema(options: []) - } catch let error as NSError { - print(error.localizedDescription) - } -#endif - let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: DataContainer.groupId)! -#if os(macOS) - let description = NSPersistentStoreDescription(url: url.appendingPathComponent("ChineseTime")) -#elseif os(iOS) - let description = NSPersistentStoreDescription(url: url.appendingPathComponent("ChineseTime.sqlite")) -#elseif os(watchOS) - let description = NSPersistentStoreDescription(url: url.appendingPathComponent("ChineseTime.sqlite")) -#endif - description.configuration = "Cloud" - description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.YLiu.ChineseTime") - container.persistentStoreDescriptions = [description] - container.loadPersistentStores(completionHandler: { _, error in - if let error = error { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error)") - } - }) - return container - }() - - func saveContext() { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - } - } - - var deviceName: String { -#if os(macOS) - return SCDynamicStoreCopyComputerName(nil, nil).map { String($0) } ?? "Mac" -#elseif os(iOS) - return UIDevice.current.name -#elseif os(watchOS) - return WKInterfaceDevice.current().name -#endif - } - - func present(error: NSError) { - print(error.localizedDescription, error.userInfo) - } - - func readSave(name: String? = nil, deviceName: String? = nil) -> String? { - try? persistentContainer.viewContext.setQueryGenerationFrom(.current) - let managedContext = persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "Layout") - fetchRequest.predicate = NSPredicate(format: "(name == %@) AND (deviceName == %@)", argumentArray: [name ?? NSLocalizedString("Default", comment: "Default save file name"), deviceName ?? self.deviceName]) - if let fetchedEntities = try? managedContext.fetch(fetchRequest), - let retrievedLayout = fetchedEntities.last?.value(forKey: "code") as? String - { - return retrievedLayout - } else { - return nil - } - } - - func loadSave(name: String? = nil, deviceName: String? = nil) { - try? persistentContainer.viewContext.setQueryGenerationFrom(.current) - let managedContext = persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "Layout") - var appDeviceName: String? = nil - if let userDefault = UserDefaults(suiteName: DataContainer.groupId) { - appDeviceName = userDefault.string(forKey: "deviceName") - } - fetchRequest.predicate = NSPredicate(format: "(name == %@) AND (deviceName == %@)", argumentArray: [name ?? NSLocalizedString("Default", comment: "Default save file name"), (deviceName ?? appDeviceName) ?? self.deviceName]) - if let fetchedEntities = try? managedContext.fetch(fetchRequest), - let savedLayout = fetchedEntities.last?.value(forKey: "code") as? String - { - WatchLayout.shared.update(from: savedLayout) - } else { - let filePath = Bundle.main.path(forResource: "layout", ofType: "txt")! - let defaultLayout = try! String(contentsOfFile: filePath) - WatchLayout.shared.update(from: defaultLayout) - } - } - - struct SavedTheme { - var name: String - let deviceName: String - var modifiedDate: Date - } - - func listAll() -> [SavedTheme] { - try? persistentContainer.viewContext.setQueryGenerationFrom(.current) - let managedContext = persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "Layout") - var results = [SavedTheme]() - if let fetchedEntities = try? managedContext.fetch(fetchRequest) { - for entity in fetchedEntities { - results.append(SavedTheme(name: (entity.value(forKey: "name") as? String) ?? NSLocalizedString("神祕檔", comment: "Unknown saved file"), - deviceName: (entity.value(forKey: "deviceName") as? String) ?? "", - modifiedDate: (entity.value(forKey: "modifiedDate") as? Date) ?? Date.distantPast)) - } - } - return results - } - - func renameSave(name: String, deviceName: String, newName: String) { - let managedContext = persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "Layout") - fetchRequest.predicate = NSPredicate(format: "(name == %@) AND (deviceName == %@)", argumentArray: [name, deviceName]) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "modifiedDate", ascending: true)] - if let fetchedEntities = try? managedContext.fetch(fetchRequest), - fetchedEntities.count > 0 - { - let savedLayout = fetchedEntities.last! - savedLayout.setValue(newName, forKey: "name") - do { - try managedContext.save() - } catch let error as NSError { - present(error: error) - } - } - } - - func deleteSave(name: String, deviceName: String) { - let managedContext = persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "Layout") - if name == NSLocalizedString("神祕檔", comment: "Unknown saved file") { - fetchRequest.predicate = NSPredicate(format: "name == NULL OR name == ''") - } else { - fetchRequest.predicate = NSPredicate(format: "(name == %@) AND (deviceName == %@)", argumentArray: [name, deviceName]) - } - if let fetchedEntities = try? managedContext.fetch(fetchRequest) { - for i in 0..(entityName: "Layout") - fetchRequest.predicate = NSPredicate(format: "(name == %@) AND (deviceName == %@)", argumentArray: [name ?? NSLocalizedString("Default", comment: "Default save file name"), deviceName]) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "modifiedDate", ascending: true)] - let savedLayout: NSManagedObject - if let fetchedEntities = try? managedContext.fetch(fetchRequest), fetchedEntities.count > 0 { - savedLayout = fetchedEntities.last! - for i in 0..<(fetchedEntities.count - 1) { - managedContext.delete(fetchedEntities[i]) - } - } else { - let newLayoutEntity = NSEntityDescription.entity(forEntityName: "Layout", in: managedContext)! - savedLayout = NSManagedObject(entity: newLayoutEntity, insertInto: managedContext) - } - savedLayout.setValue(layout, forKey: "code") - savedLayout.setValue(Date(), forKey: "modifiedDate") - savedLayout.setValue(name ?? NSLocalizedString("Default", comment: "Default save file name"), forKey: "name") - savedLayout.setValue(deviceName, forKey: "deviceName") - do { - try managedContext.save() - } catch let error as NSError { - present(error: error) - } - } -} diff --git a/Shared/Environments.swift b/Shared/Environments.swift new file mode 100644 index 0000000..470e25d --- /dev/null +++ b/Shared/Environments.swift @@ -0,0 +1,47 @@ +// +// File.swift +// Chinese Time +// +// Created by Leo Liu on 8/29/23. +// + +import SwiftUI + +struct WatchLayoutKey: EnvironmentKey { + static let defaultValue: WatchLayout = .shared +} + +struct WatchSettingKey: EnvironmentKey { + static let defaultValue: WatchSetting = .shared +} + +struct LocationManagerKey: EnvironmentKey { + static let defaultValue: LocationManager = .shared +} + +struct ChineseCalendarKey: EnvironmentKey { + static let defaultValue: ChineseCalendar = .init(time: .now) +} + +extension EnvironmentValues { + + var watchLayout: WatchLayout { + get { self[WatchLayoutKey.self] } + set { self[WatchLayoutKey.self] = newValue } + } + + var watchSetting: WatchSetting { + get { self[WatchSettingKey.self] } + set { self[WatchSettingKey.self] = newValue } + } + + var locationManager: LocationManager { + get { self[LocationManagerKey.self] } + set { self[LocationManagerKey.self] = newValue } + } + + var chineseCalendar: ChineseCalendar { + get { self[ChineseCalendarKey.self] } + set { self[ChineseCalendarKey.self] = newValue } + } +} diff --git a/Shared/InfoPlist.xcstrings b/Shared/InfoPlist.xcstrings new file mode 100644 index 0000000..d606921 --- /dev/null +++ b/Shared/InfoPlist.xcstrings @@ -0,0 +1,144 @@ +{ + "sourceLanguage" : "zh-Hant", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "華曆" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "華曆" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Copyright (human-readable)", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open source under GPL v3" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 GPL v3 协议开源" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "以 GPL v3 協議開源" + } + } + } + }, + "NSLocationAlwaysAndWhenInUseUsageDescription" : { + "comment" : "Privacy - Location Always and When In Use Usage Description", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。" + } + } + } + }, + "NSLocationUsageDescription" : { + "comment" : "Privacy - Location Usage Description", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "comment" : "Privacy - Location When In Use Usage Description", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Shared/Locale.swift b/Shared/Locale.swift new file mode 100644 index 0000000..88d63a2 --- /dev/null +++ b/Shared/Locale.swift @@ -0,0 +1,35 @@ +// +// Locale.swift +// Chinese Time +// +// Created by Leo Liu on 8/18/23. +// + +import Foundation + +extension Locale { + static var isChinese: Bool { + let languages = Locale.preferredLanguages + var isChinese = true + for language in languages { + let flag = language[language.startIndex.. String { - return "x: \(x), y: \(y)" - } - - init?(from str: String?) { - self.init() - guard let str = str else { return nil } - let regex = try! NSRegularExpression(pattern: "x:\\s*([\\-0-9\\.]+)\\s*,\\s*y:\\s*([\\-0-9\\.]+)", options: .allowCommentsAndWhitespace) - let matches = regex.matches(in: str, range: NSMakeRange(0, str.endIndex.utf16Offset(in: str))) - if !matches.isEmpty, - let x = Double((str as NSString).substring(with: matches[0].range(at: 1))), - let y = Double((str as NSString).substring(with: matches[0].range(at: 2))) - { - self.x = x - self.y = y - } else { - return nil - } - } -} - -protocol OptionalType { - associatedtype Wrapped - var optional: Wrapped? { get } -} - -extension Optional: OptionalType { - var optional: Self { self } -} - -extension Array where Element: OptionalType { - func flattened() -> [Element.Wrapped]? { - var newArray = [Element.Wrapped]() - for item in self { - if item.optional == nil { - return nil - } else { - newArray.append(item.optional!) - } - } - return newArray - } -} - -extension Date { - func convertToTimeZone(initTimeZone: TimeZone, timeZone: TimeZone) -> Date { - let delta = TimeInterval(timeZone.secondsFromGMT(for: self) - initTimeZone.secondsFromGMT(for: self)) - return addingTimeInterval(delta) - } -} - -extension String { - var floatValue: CGFloat? { - let string = trimmingCharacters(in: .whitespaces) - guard !string.isEmpty else { - return nil - } - return Double(string).map { CGFloat($0) } - } - - var boolValue: Bool? { - guard !isEmpty else { - return nil - } - let trimmedString = trimmingCharacters(in: .whitespaces).lowercased() - if ["true", "yes"].contains(trimmedString) { - return true - } else if ["false", "no"].contains(trimmedString) { - return false - } else { - return nil - } - } - - var colorValue: CGColor? { - let string = trimmingCharacters(in: .whitespaces) - guard !string.isEmpty else { - return nil - } - var r = 0, g = 0, b = 0, a = 0xff - if string.count == 10 { - // 0xffccbbaa - let regex = try! NSRegularExpression(pattern: "^0x([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$", options: .caseInsensitive) - let matches = regex.matches(in: string, options: .init(rawValue: 0), range: NSMakeRange(0, string.endIndex.utf16Offset(in: string))) - if matches.count == 1 { - r = Int((string as NSString).substring(with: matches[0].range(at: 4)), radix: 16)! - g = Int((string as NSString).substring(with: matches[0].range(at: 3)), radix: 16)! - b = Int((string as NSString).substring(with: matches[0].range(at: 2)), radix: 16)! - a = Int((string as NSString).substring(with: matches[0].range(at: 1)), radix: 16)! - } else { - return nil - } - } else if string.count == 8 { - // 0xccbbaa - let regex = try! NSRegularExpression(pattern: "^0x([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$", options: .caseInsensitive) - let matches = regex.matches(in: string, options: .init(rawValue: 0), range: NSMakeRange(0, string.endIndex.utf16Offset(in: string))) - if matches.count == 1 { - r = Int((string as NSString).substring(with: matches[0].range(at: 3)), radix: 16)! - g = Int((string as NSString).substring(with: matches[0].range(at: 2)), radix: 16)! - b = Int((string as NSString).substring(with: matches[0].range(at: 1)), radix: 16)! - } else { - return nil - } - } - return CGColor(colorSpace: displayP3, components: [CGFloat(r)/255, CGFloat(g)/255, CGFloat(b)/255, CGFloat(a)/255]) - } -} - -class MetaWatchLayout { - final class Gradient { - private let _locations: [CGFloat] - private let _colors: [CGColor] - let isLoop: Bool - - init(locations: [CGFloat], colors: [CGColor], loop: Bool) { - guard locations.count == colors.count else { - fatalError() - } - var colorAndLocation = Array(zip(colors, locations)) - colorAndLocation.sort { former, latter in - former.1 < latter.1 - } - _locations = colorAndLocation.map { $0.1 } - _colors = colorAndLocation.map { $0.0 } - isLoop = loop - } - - func interpolate(at: CGFloat) -> CGColor { - let locations = self.locations - let colors = self.colors - let nextIndex = locations.firstIndex { $0 >= at } - if let nextIndex = nextIndex { - let previousIndex = nextIndex.advanced(by: -1) - if previousIndex >= locations.startIndex { - let leftColor = colors[previousIndex] - let rightColor = colors[nextIndex] - let ratio = (at - locations[previousIndex])/(locations[nextIndex] - locations[previousIndex]) - guard ratio <= 1 && ratio >= 0 else { fatalError() } - let leftComponents = leftColor.converted(to: displayP3, intent: .perceptual, options: nil)!.components! - let rightComponents = rightColor.converted(to: displayP3, intent: .perceptual, options: nil)!.components! - let newComponents = zip(leftComponents, rightComponents).map { (1 - ratio) * $0.0 + ratio * $0.1 } - let newColor = CGColor(colorSpace: displayP3, components: newComponents) - return newColor! - } else { - return colors.first! - } - } else { - return colors.last! - } - } - - var locations: [CGFloat] { - if isLoop { - return _locations + [1] - } else { - return _locations - } - } - - var colors: [CGColor] { - if isLoop { - return _colors + [_colors[0]] - } else { - return _colors - } - } - - func encode() -> String { - var encoded = "" - let locationString = _locations.map { "\($0)" }.joined(separator: ", ") - encoded += "locations: \(locationString)\n" - let colorString = _colors.map { $0.hexCode }.joined(separator: ", ") - encoded += "colors: \(colorString)\n" - encoded += "loop: \(isLoop)" - return encoded - } - - init?(from str: String?) { - guard let str = str else { return nil } - let regex = try! NSRegularExpression(pattern: "([a-z_0-9]+)\\s*:[\\s\"]*([^\\s\"#][^\"#]*)[\\s\"#]*(#*.*)$", options: .caseInsensitive) - var values = [String: String]() - for line in str.split(whereSeparator: \.isNewline) { - let line = String(line) - let matches = regex.matches(in: line, options: .init(rawValue: 0), range: NSMakeRange(0, line.endIndex.utf16Offset(in: line))) - for match in matches { - values[(line as NSString).substring(with: match.range(at: 1))] = (line as NSString).substring(with: match.range(at: 2)) - } - } - guard let newLocations = values["locations"]?.split(separator: ","), let newColors = values["colors"]?.split(separator: ","), let isLoop = values["loop"]?.boolValue else { return nil } - let locations = Array(newLocations).map { $0.trimmingCharacters(in: .whitespaces).floatValue } - let colors = Array(newColors).map { $0.trimmingCharacters(in: .whitespaces).colorValue } - guard let loc = locations.flattened(), let col = colors.flattened() else { return nil } - _locations = loc - _colors = col - self.isLoop = isLoop - } - } - - var location: CGPoint? - var firstRing: Gradient - var secondRing: Gradient - var thirdRing: Gradient - var innerColor: CGColor - var majorTickColor: CGColor - var minorTickColor: CGColor - var majorTickAlpha: CGFloat - var minorTickAlpha: CGFloat - var fontColor: CGColor - var centerFontColor: Gradient - var evenSolarTermTickColor: CGColor - var oddSolarTermTickColor: CGColor - var innerColorDark: CGColor - var majorTickColorDark: CGColor - var minorTickColorDark: CGColor - var fontColorDark: CGColor - var evenSolarTermTickColorDark: CGColor - var oddSolarTermTickColorDark: CGColor - var planetIndicator: [CGColor] - var sunPositionIndicator: [CGColor] - var moonPositionIndicator: [CGColor] - var eclipseIndicator: CGColor - var fullmoonIndicator: CGColor - var oddStermIndicator: CGColor - var evenStermIndicator: CGColor - var shadeAlpha: CGFloat - var centerTextOffset: CGFloat - var centerTextHOffset: CGFloat - var verticalTextOffset: CGFloat - var horizontalTextOffset: CGFloat - var watchSize: CGSize - var cornerRadiusRatio: CGFloat - init() { - let firstRingStart = CGColor(colorSpace: displayP3, components: [178.0/255.0, 93.0/255.0, 141.0/255.0, 1.0])! - let firstRingEnd = CGColor(colorSpace: displayP3, components: [204.0/255.0, 75/255.0, 89.0/255.0, 1.0])! - firstRing = Gradient(locations: [0, 0.5], colors: [firstRingStart, firstRingEnd], loop: true) - - let secondRingStart = CGColor(colorSpace: displayP3, components: [205.0/255.0, 74.0/255.0, 94.0/255.0, 1.0])! - let secondRingEnd = CGColor(colorSpace: displayP3, components: [179.0/255.0, 98.0/255.0, 89.0/255.0, 1.0])! - secondRing = Gradient(locations: [0, 0.5], colors: [secondRingStart, secondRingEnd], loop: true) - - let thirdRingStart = CGColor(colorSpace: displayP3, components: [147.0/255.0, 102.0/255.0, 203.0/255.0, 1.0])! - let thirdRingEnd = CGColor(colorSpace: displayP3, components: [180.0/255.0, 94.0/255.0, 119.0/255.0, 1.0])! - thirdRing = Gradient(locations: [0, 0.5], colors: [thirdRingStart, thirdRingEnd], loop: true) - - let centerFontColorStart = CGColor(colorSpace: displayP3, components: [243.0/255.0, 230.0/255.0, 233.0/255.0, 1.0])! - let centerFontColorEnd = CGColor(colorSpace: displayP3, components: [219.0/255.0, 213.0/255.0, 236.0/255.0, 1.0])! - centerFontColor = Gradient(locations: [0, 1], colors: [centerFontColorStart, centerFontColorEnd], loop: false) - - innerColor = CGColor(colorSpace: displayP3, components: [143.0/255.0, 115.0/255.0, 140.0/255.0, 0.5])! - innerColorDark = CGColor(colorSpace: displayP3, components: [143.0/255.0, 115.0/255.0, 140.0/255.0, 0.5])! - majorTickColor = CGColor(gray: 0.0, alpha: 1.0) - majorTickColorDark = CGColor(gray: 0.0, alpha: 1.0) - majorTickAlpha = 1 - minorTickColor = CGColor(colorSpace: displayP3, components: [22.0/255.0, 22.0/255.0, 22.0/255.0, 1.0])! - minorTickColorDark = CGColor(colorSpace: displayP3, components: [22.0/255.0, 22.0/255.0, 22.0/255.0, 1.0])! - minorTickAlpha = 1 - fontColor = CGColor(gray: 1.0, alpha: 1.0) - fontColorDark = CGColor(gray: 1.0, alpha: 1.0) - evenSolarTermTickColor = CGColor(gray: 0.0, alpha: 1.0) - oddSolarTermTickColor = CGColor(colorSpace: displayP3, components: [102.0/255.0, 102.0/255.0, 102.0/255.0, 1.0])! - evenSolarTermTickColorDark = CGColor(gray: 1.0, alpha: 1.0) - oddSolarTermTickColorDark = CGColor(colorSpace: displayP3, components: [153.0/255.0, 153.0/255.0, 153.0/255.0, 1.0])! - planetIndicator = [CGColor(colorSpace: displayP3, components: [10.0/255.0, 30.0/255.0, 60.0/255.0, 1.0])!, // Mercury - CGColor(colorSpace: displayP3, components: [200.0/255.0, 190.0/255.0, 170.0/255.0, 1.0])!, // Venus - CGColor(colorSpace: displayP3, components: [210.0/255.0, 48.0/255.0, 40.0/255.0, 1.0])!, // Mars - CGColor(colorSpace: displayP3, components: [60.0/255.0, 180.0/255.0, 90.0/255.0, 1.0])!, // Jupyter - CGColor(colorSpace: displayP3, components: [170.0/255.0, 150.0/255.0, 50.0/255.0, 1.0])!, // Saturn - CGColor(colorSpace: displayP3, components: [220.0/255.0, 200.0/255.0, 60.0/255.0, 1.0])!] // Moon - sunPositionIndicator = [CGColor(colorSpace: displayP3, components: [0/255.0, 0/255.0, 0/255.0, 1.0])!, // Mid Night - CGColor(colorSpace: displayP3, components: [255.0/255.0, 80.0/255.0, 10.0/255.0, 1.0])!, // Sunrise - CGColor(colorSpace: displayP3, components: [210.0/255.0, 170.0/255.0, 120.0/255.0, 1.0])!, // Noon - CGColor(colorSpace: displayP3, components: [230.0/255.0, 120.0/255.0, 30.0/255.0, 1.0])!] // Sunset - moonPositionIndicator = [CGColor(colorSpace: displayP3, components: [190.0/255.0, 210.0/255.0, 30.0/255.0, 1.0])!, // Moon rise - CGColor(colorSpace: displayP3, components: [255.0/255.0, 255.0/255.0, 50.0/255.0, 1.0])!, // Moon at meridian - CGColor(colorSpace: displayP3, components: [120.0/255.0, 30.0/255.0, 150.0/255.0, 1.0])!] // Moon set - eclipseIndicator = CGColor(colorSpace: displayP3, components: [50.0/255.0, 68.0/255.0, 96.0/255.0, 1.0])! - fullmoonIndicator = CGColor(colorSpace: displayP3, components: [255.0/255.0, 239.0/255.0, 59.0/255.0, 1.0])! - oddStermIndicator = CGColor(colorSpace: displayP3, components: [153.0/255.0, 153.0/255.0, 153.0/255.0, 1.0])! - - evenStermIndicator = CGColor(gray: 1.0, alpha: 1.0) - shadeAlpha = 0.2 - centerTextOffset = -0.1 - centerTextHOffset = 0.0 - verticalTextOffset = 0.3 - horizontalTextOffset = 0.01 - watchSize = CGSize(width: 396, height: 484) - cornerRadiusRatio = 0.3 - } - - func encode(includeOffset: Bool = true) -> String { - var encoded = "" - encoded += "globalMonth: \(ChineseCalendar.globalMonth)\n" - encoded += "apparentTime: \(ChineseCalendar.apparentTime)\n" - encoded += "locationEnabled: \(LocationManager.shared.enabled)\n" - if let location = location { - encoded += "customLocation: \(location.encode())\n" - } - encoded += "firstRing: \(firstRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" - encoded += "secondRing: \(secondRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" - encoded += "thirdRing: \(thirdRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" - encoded += "innerColor: \(innerColor.hexCode)\n" - encoded += "majorTickColor: \(majorTickColor.hexCode)\n" - encoded += "majorTickAlpha: \(majorTickAlpha)\n" - encoded += "minorTickColor: \(minorTickColor.hexCode)\n" - encoded += "minorTickAlpha: \(minorTickAlpha)\n" - encoded += "fontColor: \(fontColor.hexCode)\n" - encoded += "centerFontColor: \(centerFontColor.encode().replacingOccurrences(of: "\n", with: "; "))\n" - encoded += "evenSolarTermTickColor: \(evenSolarTermTickColor.hexCode)\n" - encoded += "oddSolarTermTickColor: \(oddSolarTermTickColor.hexCode)\n" - encoded += "innerColorDark: \(innerColorDark.hexCode)\n" - encoded += "majorTickColorDark: \(majorTickColorDark.hexCode)\n" - encoded += "minorTickColorDark: \(minorTickColorDark.hexCode)\n" - encoded += "fontColorDark: \(fontColorDark.hexCode)\n" - encoded += "evenSolarTermTickColorDark: \(evenSolarTermTickColorDark.hexCode)\n" - encoded += "oddSolarTermTickColorDark: \(oddSolarTermTickColorDark.hexCode)\n" - encoded += "planetIndicator: \(planetIndicator.map { $0.hexCode }.joined(separator: ", "))\n" - encoded += "eclipseIndicator: \(eclipseIndicator.hexCode)\n" - encoded += "fullmoonIndicator: \(fullmoonIndicator.hexCode)\n" - encoded += "oddStermIndicator: \(oddStermIndicator.hexCode)\n" - encoded += "evenStermIndicator: \(evenStermIndicator.hexCode)\n" - encoded += "sunPositionIndicator: \(sunPositionIndicator.map { $0.hexCode }.joined(separator: ", "))\n" - encoded += "moonPositionIndicator: \(moonPositionIndicator.map { $0.hexCode }.joined(separator: ", "))\n" - encoded += "shadeAlpha: \(shadeAlpha)\n" - if includeOffset { - encoded += "centerTextOffset: \(centerTextOffset)\n" - encoded += "centerTextHorizontalOffset: \(centerTextHOffset)\n" - encoded += "verticalTextOffset: \(verticalTextOffset)\n" - encoded += "horizontalTextOffset: \(horizontalTextOffset)\n" - encoded += "watchWidth: \(watchSize.width)\n" - encoded += "watchHeight: \(watchSize.height)\n" - encoded += "cornerRadiusRatio: \(cornerRadiusRatio)\n" - } - return encoded - } - - func extract(from str: String) -> [String: String] { - let regex = try! NSRegularExpression(pattern: "^([a-z_0-9]+)\\s*:[\\s\"]*([^\\s\"#][^\"#]*)[\\s\"#]*(#*.*)$", options: .caseInsensitive) - var values = [String: String]() - for line in str.split(whereSeparator: \.isNewline) { - let line = String(line) - let matches = regex.matches(in: line, options: .init(rawValue: 0), range: NSMakeRange(0, line.endIndex.utf16Offset(in: line))) - for match in matches { - values[(line as NSString).substring(with: match.range(at: 1))] = (line as NSString).substring(with: match.range(at: 2)) - } - } - return values - } - - func update(from values: [String: String]) { - let seperatorRegex = try! NSRegularExpression(pattern: "(\\s*;|\\{\\})", options: .caseInsensitive) - func readGradient(value: String?) -> Gradient? { - guard let value = value else { return nil } - let mutableValue = NSMutableString(string: value) - seperatorRegex.replaceMatches(in: mutableValue, options: .init(rawValue: 0), range: NSMakeRange(0, mutableValue.length), withTemplate: "\n") - return Gradient(from: mutableValue as String) - } - - func readColorList(_ list: String?) -> [CGColor]? { - var colors = [CGColor?]() - if let colorValues = list { - for color in colorValues.split(separator: ",") { - colors.append(String(color).colorValue) - } - return colors.flattened() - } else { - return nil - } - } - - ChineseCalendar.globalMonth = values["globalMonth"]?.boolValue ?? ChineseCalendar.globalMonth - ChineseCalendar.apparentTime = values["apparentTime"]?.boolValue ?? ChineseCalendar.apparentTime - LocationManager.shared.enabled = values["locationEnabled"]?.boolValue ?? LocationManager.shared.enabled - location = CGPoint(from: values["customLocation"]) - firstRing = readGradient(value: values["firstRing"]) ?? firstRing - secondRing = readGradient(value: values["secondRing"]) ?? secondRing - thirdRing = readGradient(value: values["thirdRing"]) ?? thirdRing - innerColor = values["innerColor"]?.colorValue ?? innerColor - majorTickColor = values["majorTickColor"]?.colorValue ?? majorTickColor - majorTickAlpha = values["majorTickAlpha"]?.floatValue ?? majorTickAlpha - minorTickColor = values["minorTickColor"]?.colorValue ?? minorTickColor - minorTickAlpha = values["minorTickAlpha"]?.floatValue ?? minorTickAlpha - fontColor = values["fontColor"]?.colorValue ?? fontColor - centerFontColor = readGradient(value: values["centerFontColor"]) ?? centerFontColor - evenSolarTermTickColor = values["evenSolarTermTickColor"]?.colorValue ?? evenSolarTermTickColor - oddSolarTermTickColor = values["oddSolarTermTickColor"]?.colorValue ?? oddSolarTermTickColor - innerColorDark = values["innerColorDark"]?.colorValue ?? innerColor - majorTickColorDark = values["majorTickColorDark"]?.colorValue ?? majorTickColor - minorTickColorDark = values["minorTickColorDark"]?.colorValue ?? minorTickColor - fontColorDark = values["fontColorDark"]?.colorValue ?? fontColor - evenSolarTermTickColorDark = values["evenSolarTermTickColorDark"]?.colorValue ?? evenSolarTermTickColorDark - oddSolarTermTickColorDark = values["oddSolarTermTickColorDark"]?.colorValue ?? oddSolarTermTickColorDark - if let colourList = readColorList(values["planetIndicator"]), colourList.count == self.planetIndicator.count { - planetIndicator = colourList - } - if let colourList = readColorList(values["sunPositionIndicator"]), colourList.count == self.sunPositionIndicator.count { - sunPositionIndicator = colourList - } - if let colourList = readColorList(values["moonPositionIndicator"]), colourList.count == self.moonPositionIndicator.count { - moonPositionIndicator = colourList - } - eclipseIndicator = values["eclipseIndicator"]?.colorValue ?? eclipseIndicator - fullmoonIndicator = values["fullmoonIndicator"]?.colorValue ?? fullmoonIndicator - oddStermIndicator = values["oddStermIndicator"]?.colorValue ?? oddStermIndicator - evenStermIndicator = values["evenStermIndicator"]?.colorValue ?? evenStermIndicator - shadeAlpha = values["shadeAlpha"]?.floatValue ?? shadeAlpha - centerTextOffset = values["centerTextOffset"]?.floatValue ?? centerTextOffset - centerTextHOffset = values["centerTextHorizontalOffset"]?.floatValue ?? centerTextHOffset - verticalTextOffset = values["verticalTextOffset"]?.floatValue ?? verticalTextOffset - horizontalTextOffset = values["horizontalTextOffset"]?.floatValue ?? horizontalTextOffset - if let width = values["watchWidth"]?.floatValue, let height = values["watchHeight"]?.floatValue { - watchSize = CGSize(width: width, height: height) - } - cornerRadiusRatio = values["cornerRadiusRatio"]?.floatValue ?? cornerRadiusRatio - } - - func update(from str: String) { - let values = extract(from: str) - update(from: values) - } -} diff --git a/Shared/MetaWatchFace.swift b/Shared/MetaWatchFace.swift deleted file mode 100644 index 5e963ab..0000000 --- a/Shared/MetaWatchFace.swift +++ /dev/null @@ -1,586 +0,0 @@ -// -// MetaWatchFace.swift -// Chinese Time -// -// Created by Leo Liu on 4/27/23. -// - -import CoreGraphics -import Foundation -import QuartzCore.CoreAnimation - -final class GraphicArtifects { - static let width: CGFloat = 0.075 - static let paddedWidth: CGFloat = 0.075 - static let zeroRingWidth: CGFloat = 0.04 - static let markRadius: CGFloat = 0.012 - - var outerBound: RoundedRect? - var firstRingOuter: RoundedRect? - var firstRingInner: RoundedRect? - var secondRingOuter: RoundedRect? - var secondRingInner: RoundedRect? - var thirdRingOuter: RoundedRect? - var thirdRingInner: RoundedRect? - var fourthRingOuter: RoundedRect? - var fourthRingInner: RoundedRect? - var innerBound: RoundedRect? - var solarTermsRing: RoundedRect? - - var outerBoundPath: CGMutablePath? - var firstRingOuterPath: CGMutablePath? - var firstRingInnerPath: CGMutablePath? - var secondRingOuterPath: CGMutablePath? - var secondRingInnerPath: CGMutablePath? - var thirdRingOuterPath: CGMutablePath? - var thirdRingInnerPath: CGMutablePath? - var fourthRingOuterPath: CGMutablePath? - var fourthRingInnerPath: CGMutablePath? - var innerBoundPath: CGMutablePath? - - var outerOddLayer: CALayer? - var outerEvenLayer: CALayer? - var firstRingLayer: CALayer? - var firstRingMarks: CALayer? - var secondRingLayer: CALayer? - var secondRingMarks: CALayer? - var thirdRingLayer: CALayer? - var thirdRingMarks: CALayer? - var fourthRingLayer: CALayer? - var fourthRingMarks: CALayer? - var innerBox: CAShapeLayer? - var centerText: CALayer? -} - -final class KeyStates { - var year = -1 - var globalMonth = true - var month = -1 - var day = -1 - var yearUpdatedTime = Date() - var monthUpdatedTime = Date() - var priorHour = Date() - var dateString = "" - var timeString = "" - var timezone = -1 -} - -struct StartingPhase { - let zeroRing: CGFloat - let firstRing: CGFloat - let secondRing: CGFloat - let thirdRing: CGFloat - let fourthRing: CGFloat -} - -struct EntityNote { - var name: String - let position: CGPoint - let color: CGColor -} - -extension CALayer { - private static let majorUpdateInterval: CGFloat = 3600 - private static let minorUpdateInterval: CGFloat = majorUpdateInterval/12 - - func update(dirtyRect: CGRect, isDark: Bool, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar, graphicArtifects: GraphicArtifects, keyStates: KeyStates, phase: StartingPhase) -> [EntityNote] { - func angleMask(angle: CGFloat, startingAngle: CGFloat, in circle: RoundedRect) -> CAShapeLayer { - return shapeFrom(path: anglePath(angle: angle, startingAngle: startingAngle, in: circle)) - } - - func applyGradient(to path: CGPath, gradient: WatchLayout.Gradient, alpha: CGFloat = 1.0, angle: CGFloat? = nil, startingAngle: CGFloat, outerRing: RoundedRect? = nil) -> CAGradientLayer { - let gradientLayer = CAGradientLayer() - gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) - gradientLayer.endPoint = gradientLayer.startPoint + CGPoint(x: sin(startingAngle * CGFloat.pi * 2), y: cos(startingAngle * CGFloat.pi * 2)) - gradientLayer.type = .conic - if startingAngle >= 0 { - gradientLayer.colors = gradient.colors.reversed() - gradientLayer.locations = gradient.locations.map { NSNumber(value: Double(1 - $0)) }.reversed() - } else { - gradientLayer.colors = gradient.colors - gradientLayer.locations = gradient.locations.map { NSNumber(value: Double($0)) } - } - gradientLayer.frame = self.bounds - - let trackMask = shapeFrom(path: path) - let mask: CALayer - if let angle = angle, let outerRing = outerRing { - let angleMask = angleMask(angle: angle, startingAngle: startingAngle, in: outerRing) - angleMask.fillColor = CGColor(gray: 1.0, alpha: alpha) - angleMask.mask = trackMask - mask = CALayer() - mask.addSublayer(angleMask) - } else { - trackMask.fillColor = CGColor(gray: 1.0, alpha: alpha) - mask = trackMask - } - gradientLayer.mask = mask - return gradientLayer - } - - func changePhase(phase: CGFloat, angles: [CGFloat]) -> [CGFloat] { - return angles.map { angle in - if phase >= 0 { - return (angle + phase) % 1.0 - } else { - return (-angle + phase) % 1.0 - } - } - } - - func drawMark(at locations: [ChineseCalendar.NamedPosition], on ring: RoundedRect, startingAngle: CGFloat, maskPath: CGPath, colors: [CGColor], radius: CGFloat, positions: inout [EntityNote]) -> CALayer { - let marks = CALayer() - let validLocations = locations.filter { $0.pos >= 0 && $0.pos < 1 } - let points = ring.arcPoints(lambdas: changePhase(phase: startingAngle, angles: validLocations.map { CGFloat($0.pos) })) - for i in 0.. CALayer { - let marks = CALayer() - marks.addSublayer(drawMark(at: position.eclipse, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: [watchLayout.eclipseIndicator], radius: radius, positions: &positions)) - marks.addSublayer(drawMark(at: position.fullMoon, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: [watchLayout.fullmoonIndicator], radius: radius, positions: &positions)) - marks.addSublayer(drawMark(at: position.oddSolarTerm, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: [watchLayout.oddStermIndicator], radius: radius, positions: &positions)) - marks.addSublayer(drawMark(at: position.evenSolarTerm, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: [watchLayout.evenStermIndicator], radius: radius, positions: &positions)) - return marks - } - - func addIntradayMarks(locations: ChineseCalendar.DailyEvent, on ring: RoundedRect, startingAngle: CGFloat, maskPath: CGPath, radius: CGFloat, positions: inout [EntityNote]) -> CALayer { - let (sunPositionsInDay, sunPositionsInDayColors) = pairMarkPositionColor(rawPositions: locations.solar, rawColors: watchLayout.sunPositionIndicator) - let (moonPositionsInDay, moonPositionsInDayColors) = pairMarkPositionColor(rawPositions: locations.lunar, rawColors: watchLayout.moonPositionIndicator) - let marks = CALayer() - marks.addSublayer(drawMark(at: sunPositionsInDay, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: sunPositionsInDayColors, radius: radius, positions: &positions)) - marks.addSublayer(drawMark(at: moonPositionsInDay, on: ring, startingAngle: startingAngle, maskPath: maskPath, colors: moonPositionsInDayColors, radius: radius, positions: &positions)) - return marks - } - - func drawText(str: String, at: CGPoint, angle: CGFloat, color: CGColor, size: CGFloat) -> (CALayer, CGPath) { - let font = watchLayout.textFont.withSize(size) - let textLayer = CATextLayer() - var attrStr = NSMutableAttributedString(string: str) - attrStr.addAttributes([.font: font, .foregroundColor: color], range: NSMakeRange(0, str.utf16.count)) - var box = attrStr.boundingRect(with: CGSizeZero, options: .usesLineFragmentOrigin, context: .none) - box.origin = CGPoint(x: at.x - box.width/2, y: at.y - box.height/2) - if (angle > CGFloat.pi/4 && angle < CGFloat.pi * 3/4) || (angle > CGFloat.pi * 5/4 && angle < CGFloat.pi * 7/4) { - let shift = pow(size, 0.9) * watchLayout.verticalTextOffset - textLayer.frame = CGRect(x: at.x - box.width/2 - shift, y: at.y - box.height * 1.8/2, width: box.width, height: box.height * 1.8) - attrStr.addAttributes([.verticalGlyphForm: 1], range: NSMakeRange(0, str.utf16.count)) - } else { - let shift = pow(size, 0.9) * watchLayout.horizontalTextOffset - textLayer.frame = CGRect(x: at.x - box.width/2, y: at.y - box.height/2 + shift, width: box.width, height: box.height) - attrStr = NSMutableAttributedString(string: String(str.reversed()), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) - } - textLayer.string = attrStr - textLayer.contentsScale = 3 - textLayer.alignmentMode = .center - var boxTransform = CGAffineTransform(translationX: -at.x, y: -at.y) - let transform: CGAffineTransform - if angle <= CGFloat.pi/4 { - transform = CGAffineTransform(rotationAngle: -angle) - } else if angle < CGFloat.pi * 3/4 { - transform = CGAffineTransform(rotationAngle: CGFloat.pi - angle) - } else if angle < CGFloat.pi * 5/4 { - transform = CGAffineTransform(rotationAngle: CGFloat.pi - angle) - } else if angle < CGFloat.pi * 7/4 { - transform = CGAffineTransform(rotationAngle: -angle) - } else { - transform = CGAffineTransform(rotationAngle: -angle) - } - boxTransform = boxTransform.concatenating(transform) - boxTransform = boxTransform.concatenating(CGAffineTransform(translationX: at.x, y: at.y)) - textLayer.setAffineTransform(transform) - let cornerSize = 0.2 * min(box.height, box.width) - let path = CGPath(roundedRect: box, cornerWidth: cornerSize, cornerHeight: cornerSize, transform: &boxTransform) - let finishedRingLayer = CALayer() - finishedRingLayer.addSublayer(textLayer) - return (finishedRingLayer, path) - } - - func drawCenterText(str: String, offset: CGFloat, size: CGFloat, rotate: Bool) -> CATextLayer { - let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) - let font = watchLayout.centerFont.withSize(size) - let textLayer = CATextLayer() - var attrStr = NSMutableAttributedString(string: str) - attrStr.addAttributes([.font: font, .foregroundColor: CGColor(gray: 1, alpha: 1)], range: NSMakeRange(0, str.utf16.count)) - let box = attrStr.boundingRect(with: CGSizeZero, options: .usesLineFragmentOrigin, context: .none) - if rotate { - textLayer.frame = CGRect(x: center.x - box.width/2 - offset, y: center.y - box.height * 2.3/2, width: box.width, height: box.height * 2.3) - textLayer.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat.pi/2)) - attrStr.addAttributes([.verticalGlyphForm: 1], range: NSMakeRange(0, str.utf16.count)) - } else { - textLayer.frame = CGRect(x: center.x - box.width/2, y: center.y - box.height/2 + offset, width: box.width, height: box.height) - attrStr = NSMutableAttributedString(string: String(str.reversed()), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) - } - textLayer.string = attrStr - textLayer.contentsScale = 3 - textLayer.alignmentMode = .center - - return textLayer - } - - func shapeFrom(path: CGPath) -> CAShapeLayer { - let shape = CAShapeLayer() - shape.path = path - shape.fillRule = .evenOdd - return shape - } - - func drawRing(ringPath: CGPath, roundedRect: RoundedRect, gradient: WatchLayout.Gradient, ticks: ChineseCalendar.Ticks, startingAngle: CGFloat, fontSize: CGFloat, minorLineWidth: CGFloat, majorLineWidth: CGFloat, drawShadow: Bool) -> CALayer { - let ringLayer = CALayer() - let ringShadow = applyGradient(to: ringPath, gradient: gradient, alpha: watchLayout.shadeAlpha, startingAngle: startingAngle) - ringLayer.addSublayer(ringShadow) - - let ringMinorTicksPath = roundedRect.arcPosition(lambdas: changePhase(phase: startingAngle, angles: ticks.minorTicks.map { CGFloat($0) }), width: 0.1 * shortEdge) - let ringMinorTicks = CAShapeLayer() - ringMinorTicks.path = ringMinorTicksPath - - let ringMinorTrackOuter = roundedRect.shrink(by: 0.01 * shortEdge) - let ringMinorTrackInner = roundedRect.shrink(by: (GraphicArtifects.paddedWidth - 0.015) * shortEdge) - let ringMinorTrackPath = ringMinorTrackOuter.path - ringMinorTrackPath.addPath(ringMinorTrackInner.path) - - ringMinorTicks.strokeColor = isDark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor - ringMinorTicks.lineWidth = minorLineWidth - - let ringMinorTicksMaskPath = CGMutablePath() - ringMinorTicksMaskPath.addPath(ringPath) - ringMinorTicksMaskPath.addPath(ringMinorTicksPath.copy(strokingWithWidth: minorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - let ringMinorTicksMask = shapeFrom(path: ringMinorTicksMaskPath) - - let ringCrustPath = CGMutablePath() - ringCrustPath.addPath(ringMinorTrackPath) - ringCrustPath.addPath(ringPath) - let ringCrust = shapeFrom(path: ringCrustPath) - let ringBase = shapeFrom(path: ringPath) - ringBase.fillColor = CGColor(gray: 1.0, alpha: watchLayout.minorTickAlpha) - ringMinorTicksMask.addSublayer(ringCrust) - ringMinorTicksMask.addSublayer(ringBase) - - let ringMajorTicksPath = roundedRect.arcPosition(lambdas: changePhase(phase: startingAngle, angles: ticks.majorTicks.map { CGFloat($0) }), width: 0.15 * shortEdge) - let ringMajorTicks = CAShapeLayer() - ringMajorTicks.path = ringMajorTicksPath - - ringMajorTicks.strokeColor = isDark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor - ringMajorTicks.lineWidth = majorLineWidth - - ringLayer.mask = ringMinorTicksMask - - let ringLayerAfterMinor = CALayer() - ringLayerAfterMinor.addSublayer(ringLayer) - - let ringMajorTicksMaskPath = CGMutablePath() - ringMajorTicksMaskPath.addPath(ringPath) - ringMajorTicksMaskPath.addPath(ringMajorTicksPath.copy(strokingWithWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - let ringMajorTicksMask = shapeFrom(path: ringMajorTicksMaskPath) - let ringBase2 = shapeFrom(path: ringPath) - ringBase2.fillColor = CGColor(gray: 1.0, alpha: watchLayout.majorTickAlpha) - ringMajorTicksMask.addSublayer(ringBase2) - ringLayerAfterMinor.mask = ringMajorTicksMask - - let finishedRingLayer = CALayer() - let textLayers = CALayer() - let shadowLayer = CALayer() - finishedRingLayer.addSublayer(ringLayerAfterMinor) - finishedRingLayer.addSublayer(shadowLayer) - finishedRingLayer.addSublayer(ringMinorTicks) - finishedRingLayer.addSublayer(ringMajorTicks) - finishedRingLayer.addSublayer(textLayers) - - let textRing = roundedRect.shrink(by: (GraphicArtifects.paddedWidth - 0.005)/2 * shortEdge) - let textPoints = textRing.arcPoints(lambdas: changePhase(phase: startingAngle, angles: ticks.majorTickNames.map { CGFloat($0.position) })) - let textMaskPath = CGMutablePath() - let fontColor = isDark ? watchLayout.fontColorDark : watchLayout.fontColor - for i in 0.. CALayer { - let ringPath = roundedRect.path - ringPath.addPath(path) - - let ringShape = shapeFrom(path: ringPath) - let ringTicks = roundedRect.arcPosition(lambdas: changePhase(phase: startingAngle, angles: tickPositions), width: 0.1 * shortEdge) - let ringTicksShape = shapeFrom(path: ringTicks) - ringTicksShape.mask = ringShape - ringTicksShape.strokeColor = color - ringTicksShape.lineWidth = lineWidth - let finishedRingLayer = CALayer() - finishedRingLayer.addSublayer(ringTicksShape) - - var i = 0 - let points = textRoundedRect.arcPoints(lambdas: changePhase(phase: startingAngle, angles: tickPositions)) - for point in points { - let (textLayer, _) = drawText(str: texts[i], at: point.position, angle: point.direction, color: color, size: fontSize) - finishedRingLayer.addSublayer(textLayer) - i += 1 - } - return finishedRingLayer - } - - func calSubhourGradient() -> WatchLayout.Gradient { - let startOfDay = chineseCalendar.startOfDay - let lengthOfDay = startOfDay.distance(to: chineseCalendar.startOfNextDay) - let fourthRingColor = WatchLayout.Gradient(locations: [0, 1], colors: [ - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.startHour)/lengthOfDay) % 1.0), - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.endHour)/lengthOfDay) % 1.0) - ], loop: false) - return fourthRingColor - } - - func pairMarkPositionColor(rawPositions: [ChineseCalendar.NamedPosition?], rawColors: [CGColor]) -> ([ChineseCalendar.NamedPosition], [CGColor]) { - var newPositions = [ChineseCalendar.NamedPosition]() - var newColors = [CGColor]() - for i in 0.. CALayer { - let centerText = CALayer() - let centerTextShortSize = min(innerBound._boundBox.width, innerBound._boundBox.height) * 0.31 - let centerTextLongSize = max(innerBound._boundBox.width, innerBound._boundBox.height) * 0.17 - let centerTextSize = min(centerTextShortSize, centerTextLongSize) - let isVertical = innerBound._boundBox.height >= innerBound._boundBox.width - let centerOffset = isVertical ? watchLayout.centerTextOffset : watchLayout.centerTextHOffset - let dateTextLayer = drawCenterText(str: timeString, offset: centerTextSize * (0.7 + centerOffset), size: centerTextSize, rotate: isVertical) - centerText.addSublayer(dateTextLayer) - let timeTextLayer = drawCenterText(str: dateString, offset: centerTextSize * (-0.7 + centerOffset), size: centerTextSize, rotate: isVertical) - centerText.addSublayer(timeTextLayer) - - let gradientLayer = CAGradientLayer() - gradientLayer.startPoint = CGPoint(x: -0.3, y: 0.3) - gradientLayer.endPoint = CGPoint(x: 0.3, y: -0.3) - gradientLayer.type = .axial - gradientLayer.colors = watchLayout.centerFontColor.colors - gradientLayer.locations = watchLayout.centerFontColor.locations.map { NSNumber(value: Double($0)) } - gradientLayer.frame = self.bounds - gradientLayer.mask = centerText - - return gradientLayer as CALayer - } - - func getVagueShapes(shortEdge: CGFloat, longEdge: CGFloat) { - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - // Basic paths - graphicArtifects.outerBound = RoundedRect(rect: dirtyRect, nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: 0.02 * shortEdge) - graphicArtifects.solarTermsRing = graphicArtifects.outerBound!.shrink(by: (GraphicArtifects.zeroRingWidth + 0.003)/2 * shortEdge) - - graphicArtifects.firstRingOuter = graphicArtifects.outerBound!.shrink(by: GraphicArtifects.zeroRingWidth * shortEdge) - graphicArtifects.firstRingInner = graphicArtifects.firstRingOuter!.shrink(by: GraphicArtifects.width * shortEdge) - - graphicArtifects.secondRingOuter = graphicArtifects.firstRingOuter!.shrink(by: GraphicArtifects.paddedWidth * shortEdge) - graphicArtifects.secondRingInner = graphicArtifects.secondRingOuter!.shrink(by: GraphicArtifects.width * shortEdge) - - graphicArtifects.thirdRingOuter = graphicArtifects.secondRingOuter!.shrink(by: GraphicArtifects.paddedWidth * shortEdge) - graphicArtifects.thirdRingInner = graphicArtifects.thirdRingOuter!.shrink(by: GraphicArtifects.width * shortEdge) - - graphicArtifects.fourthRingOuter = graphicArtifects.thirdRingOuter!.shrink(by: GraphicArtifects.paddedWidth * shortEdge) - graphicArtifects.fourthRingInner = graphicArtifects.fourthRingOuter!.shrink(by: GraphicArtifects.width * shortEdge) - - graphicArtifects.innerBound = graphicArtifects.fourthRingOuter!.shrink(by: GraphicArtifects.paddedWidth * shortEdge) - - graphicArtifects.outerBoundPath = graphicArtifects.outerBound!.path - graphicArtifects.firstRingOuterPath = graphicArtifects.firstRingOuter!.path - graphicArtifects.firstRingInnerPath = graphicArtifects.firstRingInner!.path - graphicArtifects.firstRingOuterPath!.addPath(graphicArtifects.firstRingInnerPath!) - - graphicArtifects.secondRingOuterPath = graphicArtifects.secondRingOuter!.path - graphicArtifects.secondRingInnerPath = graphicArtifects.secondRingInner!.path - graphicArtifects.secondRingOuterPath!.addPath(graphicArtifects.secondRingInnerPath!) - - graphicArtifects.thirdRingOuterPath = graphicArtifects.thirdRingOuter!.path - graphicArtifects.thirdRingInnerPath = graphicArtifects.thirdRingInner!.path - graphicArtifects.thirdRingOuterPath!.addPath(graphicArtifects.thirdRingInnerPath!) - - graphicArtifects.fourthRingOuterPath = graphicArtifects.fourthRingOuter!.path - graphicArtifects.fourthRingInnerPath = graphicArtifects.fourthRingInner!.path - graphicArtifects.fourthRingOuterPath!.addPath(graphicArtifects.fourthRingInnerPath!) - - graphicArtifects.innerBoundPath = graphicArtifects.innerBound!.path - } - - let shortEdge = min(dirtyRect.width, dirtyRect.height) - let longEdge = max(dirtyRect.width, dirtyRect.height) - let fontSize: CGFloat = min(shortEdge * 0.03, longEdge * 0.025) - let minorLineWidth = shortEdge/500 - let majorLineWidth = shortEdge/300 - let shadowDirection = chineseCalendar.currentHourInDay - - if graphicArtifects.outerBound == nil { - getVagueShapes(shortEdge: shortEdge, longEdge: longEdge) - } - - var markPositions = [EntityNote]() - - // Zero ring - if (graphicArtifects.outerOddLayer == nil) || (chineseCalendar.year != keyStates.year) { - let oddSolarTermTickColor = isDark ? watchLayout.oddSolarTermTickColorDark : watchLayout.oddSolarTermTickColor - let evenSolarTermTickColor = isDark ? watchLayout.evenSolarTermTickColorDark : watchLayout.evenSolarTermTickColor - - graphicArtifects.outerOddLayer = drawOuterRing(path: graphicArtifects.firstRingOuterPath!, roundedRect: graphicArtifects.outerBound!, textRoundedRect: graphicArtifects.solarTermsRing!, tickPositions: chineseCalendar.oddSolarTerms.map { CGFloat($0) }, texts: ChineseCalendar.oddSolarTermChinese, startingAngle: phase.zeroRing, fontSize: fontSize, lineWidth: majorLineWidth, color: oddSolarTermTickColor) - graphicArtifects.outerEvenLayer = drawOuterRing(path: graphicArtifects.firstRingOuterPath!, roundedRect: graphicArtifects.outerBound!, textRoundedRect: graphicArtifects.solarTermsRing!, tickPositions: chineseCalendar.evenSolarTerms.map { CGFloat($0) }, texts: ChineseCalendar.evenSolarTermChinese, startingAngle: phase.zeroRing, fontSize: fontSize, lineWidth: majorLineWidth, color: evenSolarTermTickColor) - } - self.addSublayer(graphicArtifects.outerOddLayer!) - self.addSublayer(graphicArtifects.outerEvenLayer!) - - // First Ring - if (graphicArtifects.firstRingLayer == nil) || (chineseCalendar.year != keyStates.year) || (ChineseCalendar.globalMonth != keyStates.globalMonth) || (chineseCalendar.timezone != keyStates.timezone) || (abs(chineseCalendar.time.distance(to: keyStates.yearUpdatedTime)) >= Self.majorUpdateInterval) || (chineseCalendar.preciseMonth != keyStates.month) { - let monthTicks = chineseCalendar.monthTicks - if (graphicArtifects.firstRingLayer == nil) || (chineseCalendar.year != keyStates.year) || (ChineseCalendar.globalMonth != keyStates.globalMonth) || (chineseCalendar.timezone != keyStates.timezone) { - graphicArtifects.firstRingLayer = drawRing(ringPath: graphicArtifects.firstRingOuterPath!, roundedRect: graphicArtifects.firstRingOuter!, gradient: watchLayout.firstRing, ticks: monthTicks, startingAngle: phase.firstRing, fontSize: fontSize, minorLineWidth: minorLineWidth, majorLineWidth: majorLineWidth, drawShadow: true) - keyStates.year = chineseCalendar.year - } - activeRingAngle(to: graphicArtifects.firstRingLayer!, ringPath: graphicArtifects.firstRingOuterPath!, gradient: watchLayout.firstRing, angle: chineseCalendar.currentDayInYear, startingAngle: phase.firstRing, outerRing: graphicArtifects.firstRingOuter!, ticks: monthTicks) - keyStates.yearUpdatedTime = chineseCalendar.time - } - graphicArtifects.firstRingMarks = drawMark(at: chineseCalendar.planetPosition, on: graphicArtifects.firstRingOuter!, startingAngle: phase.firstRing, maskPath: graphicArtifects.firstRingOuterPath!, colors: watchLayout.planetIndicator, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions) - self.addSublayer(graphicArtifects.firstRingLayer!) - self.addSublayer(graphicArtifects.firstRingMarks!) - - // Second Ring - if (graphicArtifects.secondRingLayer == nil) || (chineseCalendar.year != keyStates.year) || (chineseCalendar.preciseMonth != keyStates.month) || (ChineseCalendar.globalMonth != keyStates.globalMonth) || (chineseCalendar.timezone != keyStates.timezone) || (abs(chineseCalendar.time.distance(to: keyStates.monthUpdatedTime)) >= Self.minorUpdateInterval) || (chineseCalendar.day != keyStates.day) { - let dayTicks = chineseCalendar.dayTicks - if (graphicArtifects.secondRingLayer == nil) || (chineseCalendar.year != keyStates.year) || (chineseCalendar.preciseMonth != keyStates.month) || (chineseCalendar.timezone != keyStates.timezone) || (ChineseCalendar.globalMonth != keyStates.globalMonth) { - graphicArtifects.secondRingLayer = drawRing(ringPath: graphicArtifects.secondRingOuterPath!, roundedRect: graphicArtifects.secondRingOuter!, gradient: watchLayout.secondRing, ticks: dayTicks, startingAngle: phase.secondRing, fontSize: fontSize, minorLineWidth: minorLineWidth, majorLineWidth: majorLineWidth, drawShadow: true) - keyStates.month = chineseCalendar.preciseMonth - keyStates.globalMonth = ChineseCalendar.globalMonth - } - activeRingAngle(to: graphicArtifects.secondRingLayer!, ringPath: graphicArtifects.secondRingOuterPath!, gradient: watchLayout.secondRing, angle: chineseCalendar.currentDayInMonth, startingAngle: phase.secondRing, outerRing: graphicArtifects.secondRingOuter!, ticks: dayTicks) - keyStates.monthUpdatedTime = chineseCalendar.time - } - graphicArtifects.secondRingMarks = addMarks(position: chineseCalendar.eventInMonth, on: graphicArtifects.secondRingOuter!, startingAngle: phase.secondRing, maskPath: graphicArtifects.secondRingOuterPath!, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions) - self.addSublayer(graphicArtifects.secondRingLayer!) - self.addSublayer(graphicArtifects.secondRingMarks!) - - // Third Ring - let hourTicks = chineseCalendar.hourTicks - if (graphicArtifects.thirdRingLayer == nil) || (chineseCalendar.dateString != keyStates.dateString) || (chineseCalendar.year != keyStates.year) || (chineseCalendar.timezone != keyStates.timezone) { - graphicArtifects.thirdRingLayer = drawRing(ringPath: graphicArtifects.thirdRingOuterPath!, roundedRect: graphicArtifects.thirdRingOuter!, gradient: watchLayout.thirdRing, ticks: hourTicks, startingAngle: phase.thirdRing, fontSize: fontSize, minorLineWidth: minorLineWidth, majorLineWidth: majorLineWidth, drawShadow: true) - keyStates.day = chineseCalendar.day - keyStates.dateString = chineseCalendar.dateString - } - graphicArtifects.thirdRingMarks = addMarks(position: chineseCalendar.eventInDay, on: graphicArtifects.thirdRingOuter!, startingAngle: phase.thirdRing, maskPath: graphicArtifects.thirdRingOuterPath!, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions) - graphicArtifects.thirdRingMarks?.addSublayer(addIntradayMarks(locations: chineseCalendar.sunMoonPositions, on: graphicArtifects.thirdRingInner!, startingAngle: phase.thirdRing, maskPath: graphicArtifects.thirdRingOuterPath!, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions)) - activeRingAngle(to: graphicArtifects.thirdRingLayer!, ringPath: graphicArtifects.thirdRingOuterPath!, gradient: watchLayout.thirdRing, angle: chineseCalendar.currentHourInDay, startingAngle: phase.thirdRing, outerRing: graphicArtifects.thirdRingOuter!, ticks: hourTicks) - self.addSublayer(graphicArtifects.thirdRingLayer!) - self.addSublayer(graphicArtifects.thirdRingMarks!) - - // Fourth Ring - let fourthRingColor = calSubhourGradient() - let subhourTicks = chineseCalendar.subhourTicks - if (graphicArtifects.fourthRingLayer == nil) || (chineseCalendar.startHour != keyStates.priorHour) || (chineseCalendar.timezone != keyStates.timezone) { - if (graphicArtifects.fourthRingLayer == nil) || (chineseCalendar.startHour != keyStates.priorHour) { - graphicArtifects.fourthRingLayer = drawRing(ringPath: graphicArtifects.fourthRingOuterPath!, roundedRect: graphicArtifects.fourthRingOuter!, gradient: fourthRingColor, ticks: subhourTicks, startingAngle: phase.fourthRing, fontSize: fontSize, minorLineWidth: minorLineWidth, majorLineWidth: majorLineWidth, drawShadow: true) - keyStates.priorHour = chineseCalendar.startHour - } - keyStates.timezone = chineseCalendar.timezone - } - graphicArtifects.fourthRingMarks = addMarks(position: chineseCalendar.eventInHour, on: graphicArtifects.fourthRingOuter!, startingAngle: phase.fourthRing, maskPath: graphicArtifects.fourthRingOuterPath!, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions) - graphicArtifects.fourthRingMarks?.addSublayer(addIntradayMarks(locations: chineseCalendar.sunMoonSubhourPositions, on: graphicArtifects.fourthRingInner!, startingAngle: phase.fourthRing, maskPath: graphicArtifects.fourthRingOuterPath!, radius: GraphicArtifects.markRadius * shortEdge, positions: &markPositions)) - activeRingAngle(to: graphicArtifects.fourthRingLayer!, ringPath: graphicArtifects.fourthRingOuterPath!, gradient: fourthRingColor, angle: chineseCalendar.subhourInHour, startingAngle: phase.fourthRing, outerRing: graphicArtifects.fourthRingOuter!, ticks: subhourTicks) - self.addSublayer(graphicArtifects.fourthRingLayer!) - self.addSublayer(graphicArtifects.fourthRingMarks!) - - // Inner Ring - if graphicArtifects.innerBox == nil { - graphicArtifects.innerBox = shapeFrom(path: graphicArtifects.innerBoundPath!) - graphicArtifects.innerBox!.fillColor = isDark ? watchLayout.innerColorDark : watchLayout.innerColor - let shadowLayer = CALayer() - shadowLayer.shadowPath = graphicArtifects.innerBoundPath! - shadowLayer.shadowOffset = CGSize(width: -0.014 * sin(CGFloat.pi * 2 * shadowDirection) * shortEdge, height: -0.014 * cos(CGFloat.pi * 2 * shadowDirection) * shortEdge) - shadowLayer.shadowRadius = 0.03 * shortEdge - shadowLayer.shadowOpacity = isDark ? 0.5 : 0.3 - let shadowMaskPath = CGMutablePath() - shadowMaskPath.addPath(graphicArtifects.innerBoundPath!) - shadowMaskPath.addPath(CGPath(rect: self.bounds, transform: nil)) - let shadowMask = shapeFrom(path: shadowMaskPath) - shadowLayer.mask = shadowMask - graphicArtifects.innerBox?.addSublayer(shadowLayer) - } - self.addSublayer(graphicArtifects.innerBox!) - - // Center text - let timeString = chineseCalendar.timeString - let dateString = chineseCalendar.dateString - if (graphicArtifects.centerText == nil) || (dateString != keyStates.dateString) || (timeString != keyStates.timeString) { - graphicArtifects.centerText = drawCenterTextGradient(innerBound: graphicArtifects.innerBound!, dateString: dateString, timeString: timeString) - keyStates.timeString = timeString - } - self.addSublayer(graphicArtifects.centerText!) - return markPositions - } -} diff --git a/Shared/Setting/ColorSetting.swift b/Shared/Setting/ColorSetting.swift new file mode 100644 index 0000000..7de5b6f --- /dev/null +++ b/Shared/Setting/ColorSetting.swift @@ -0,0 +1,103 @@ +// +// ColorSetting.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/23/23. +// + +import SwiftUI + +struct ColorSettingCell: View { + let text: Text + @Binding var color: CGColor + + var body: some View { + HStack { + text + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 10) + ColorPicker("", selection: $color) + .labelsHidden() + } + } +} + +struct ColorSetting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + + var body: some View { + Form { + Section(header: Text("五星", comment: "Planets")) { + HStack() { + ColorSettingCell(text: Text("辰", comment: "Mercury Indicator"), color: watchLayout.binding(\.planetIndicator[0])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("太白", comment: "Venus Indicator"), color: watchLayout.binding(\.planetIndicator[1])) + } + HStack() { + ColorSettingCell(text: Text("熒惑", comment: "Mars Indicator"), color: watchLayout.binding(\.planetIndicator[2])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("歲", comment: "Jupiter Indicator"), color: watchLayout.binding(\.planetIndicator[3])) + } + HStack() { + ColorSettingCell(text: Text("鎮", comment: "Saturn Indicator"), color: watchLayout.binding(\.planetIndicator[4])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("月", comment: "Moon position Indicator"), color: watchLayout.binding(\.planetIndicator[5])) + } + } + + Section(header: Text("朔望節氣", comment: "Moon phase and Solor terms")) { + HStack { + ColorSettingCell(text: Text("朔", comment: "New Moon Indicator"), color: watchLayout.binding(\.eclipseIndicator)) + Spacer(minLength: 30) + ColorSettingCell(text: Text("望", comment: "Full Moon Indicator"), color: watchLayout.binding(\.fullmoonIndicator)) + } + HStack { + ColorSettingCell(text: Text("節氣", comment: "Odd Solar Term Indicator"), color: watchLayout.binding(\.oddStermIndicator)) + Spacer(minLength: 30) + ColorSettingCell(text: Text("中氣", comment: "Even Solar Term Indicator"), color: watchLayout.binding(\.evenStermIndicator)) + } + } + + Section(header: Text("日出入", comment: "Sunrise & Sunset Indicators")) { + HStack { + ColorSettingCell(text: Text("日出", comment: "Sunrise Indicator"), color: watchLayout.binding(\.sunPositionIndicator[1])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("日中", comment: "Noon Indicator"), color: watchLayout.binding(\.sunPositionIndicator[2])) + } + HStack { + ColorSettingCell(text: Text("日入", comment: "Sunset Indicator"), color: watchLayout.binding(\.sunPositionIndicator[3])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("夜中", comment: "Midnight Indicator"), color: watchLayout.binding(\.sunPositionIndicator[0])) + } + } + + Section(header: Text("月出入", comment: "Moonrise & Moonset")) { + HStack { + ColorSettingCell(text: Text("月出", comment: "Moonrise Indicator"), color: watchLayout.binding(\.moonPositionIndicator[0])) + Spacer(minLength: 30) + ColorSettingCell(text: Text("月中", comment: "Lunar noon Indicator"), color: watchLayout.binding(\.moonPositionIndicator[1])) + } + HStack { + ColorSettingCell(text: Text("月入", comment: "Moonset Indicator"), color: watchLayout.binding(\.moonPositionIndicator[2])) + } + } + } + .formStyle(.grouped) + .navigationTitle(Text("色塊", comment: "Mark Color settings")) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#endif + } +} + +#Preview("Color Setting") { + ColorSetting() +} diff --git a/Shared/Setting/Datetime.swift b/Shared/Setting/Datetime.swift new file mode 100644 index 0000000..902a320 --- /dev/null +++ b/Shared/Setting/Datetime.swift @@ -0,0 +1,286 @@ +// +// Datetime.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/23/23. +// + +import SwiftUI +import Observation + +fileprivate struct TimeZoneSelection: Equatable { + static func == (lhs: TimeZoneSelection, rhs: TimeZoneSelection) -> Bool { + lhs.primary == rhs.primary && lhs.secondary == rhs.secondary && lhs.tertiary == rhs.tertiary + } + + let timeZones = populateTimezones() + var primary: String { + didSet { + if let next = timeZones[primary]?.nextLevel.first?.nodeName { + secondary = next + } else { + secondary = "" + } + } + } + var secondary: String { + didSet { + if let next = timeZones[primary]?[secondary]?.nextLevel.first?.nodeName { + tertiary = next + } else { + tertiary = "" + } + } + } + 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 { "" } + secondary = if components.count > 1 { String(components[1]) } else { "" } + tertiary = if components.count > 2 { String(components[2]) } else { "" } + } + + var timezone: TimeZone? { + var identifier = primary + if secondary != "" { + identifier += "/" + secondary + } + if tertiary != "" { + identifier += "/" + tertiary + } + return TimeZone(identifier: identifier) + } +} + +@MainActor +@Observable fileprivate class DateManager { + var chineseCalendar: ChineseCalendar? + var watchSetting: WatchSetting? + var watchLayout: WatchLayout? + + var timeZoneSelection: TimeZoneSelection { + get { + TimeZoneSelection(timezone: timezone) + } set { + watchSetting?.timezone = newValue.timezone + updateTimeZone() + } + } + + var time: Date { + get { + watchSetting?.displayTime ?? chineseCalendar?.time ?? .now + } set { + watchSetting?.displayTime = newValue + updateTime() + } + } + + var timezone: TimeZone { + get { + watchSetting?.timezone ?? chineseCalendar?.calendar.timeZone ?? Calendar.current.timeZone + } + } + + var isCurrent: Bool { + get { + watchSetting?.displayTime == nil && watchSetting?.timezone == nil + } set { + if newValue { + watchSetting?.displayTime = nil + watchSetting?.timezone = nil + } else { + watchSetting?.displayTime = chineseCalendar?.time + watchSetting?.timezone = chineseCalendar?.calendar.timeZone + } + updateTimeZone() + } + } + + var globalMonth: Bool { + get { + watchLayout?.globalMonth ?? false + } set { + watchLayout?.globalMonth = newValue + updateTime() + } + } + + var apparentTime: Bool { + get { + watchLayout?.apparentTime ?? false + } set { + watchLayout?.apparentTime = newValue + updateTime() + } + } + + func setup(watchSetting: WatchSetting, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) { + self.watchLayout = watchLayout + self.watchSetting = watchSetting + self.chineseCalendar = chineseCalendar + } + + func updateTimeZone() { + chineseCalendar?.update(time: watchSetting?.displayTime ?? .now, + timezone: watchSetting?.timezone ?? Calendar.current.timeZone, location: chineseCalendar?.location) +#if os(macOS) + updateStatusBar() +#endif + } + + func updateTime() { + chineseCalendar?.update(time: watchSetting?.displayTime ?? .now) +#if os(macOS) + updateStatusBar() +#endif + } + +#if os(macOS) + @MainActor + func updateStatusBar() { + if let delegate = AppDelegate.instance, + let chineseCalendar = chineseCalendar, + let watchLayout = watchLayout { + delegate.updateStatusBar(dateText: delegate.statusBar(from: chineseCalendar, options: watchLayout)) + } + } +#endif +} + +private func populateTimezones() -> DataTree { + let root = DataTree(name: "Root") + let allTimezones = TimeZone.knownTimeZoneIdentifiers + for timezone in allTimezones { + let components = timezone.split(separator: "/") + var currentNode: DataTree? = root + for component in components { + currentNode = currentNode?.add(element: String(component)) + } + } + return root +} + +@MainActor +struct Datetime: View { + @State fileprivate var dateManager = DateManager() + @Environment(\.chineseCalendar) var chineseCalendar + @Environment(\.locationManager) var locationManager + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + + var body: some View { + Form { + Section(header: Text("算法", comment: "Methodology setting")) { + HStack { + Picker("置閏法", selection: $dateManager.globalMonth) { + ForEach([true, false], id: \.self) { globalMonth in + if globalMonth { + Text("精確至時刻", comment: "Leap month setting: precise") + } else { + Text("精確至日", comment: "Leap month setting: daily precision") + } + } + } + } + + HStack { + Picker("太陽時", selection: $dateManager.apparentTime) { + let choice = if locationManager.enabled { + [true, false] + } else { + [false] + } + ForEach(choice, id: \.self) { apparentTime in + if apparentTime { + Text("真太陽時", comment: "Time setting: apparent solar time") + } else { + Text("標準時", comment: "Time setting: mean solar time") + } + } + } + } + } + .pickerStyle(.menu) + + Section(header: Text(NSLocalizedString("日時:", comment: "Date & time section") + dateManager.time.formatted(date: .abbreviated, time: .shortened))) { + Toggle("今", isOn: $dateManager.isCurrent) + DatePicker("擇時", selection: $dateManager.time, in: ChineseCalendar.start...ChineseCalendar.end, displayedComponents: [.date, .hourAndMinute]) + .environment(\.timeZone, dateManager.timezone) + } + + let timezoneTitle = if let desp = dateManager.timezone.localizedName(for: .standard, locale: Locale.current) { + NSLocalizedString("時區:", comment: "Timezone section") + desp + } else { + NSLocalizedString("時區", comment: "Timezone section") + } + Section(header: Text(timezoneTitle)) { + HStack { + Picker("大區", selection: $dateManager.timeZoneSelection.primary) { + ForEach(dateManager.timeZoneSelection.timeZones.nextLevel.map { $0.nodeName }, id: \.self) { tz in + Text(tz.replacingOccurrences(of: "_", with: " ")) + + } + } + .lineLimit(1) + .animation(.default, value: dateManager.timeZoneSelection) + if let tzList = dateManager.timeZoneSelection.timeZones[dateManager.timeZoneSelection.primary], tzList.count > 0 { +#if os(macOS) + Spacer(minLength: 20) +#endif + Picker("中區", selection: $dateManager.timeZoneSelection.secondary) { + ForEach(tzList.nextLevel.map { $0.nodeName }, id: \.self) { tz in + Text(tz.replacingOccurrences(of: "_", with: " ")) + } + } + .lineLimit(1) + .animation(.default, value: dateManager.timeZoneSelection) + } + if let tzList = dateManager.timeZoneSelection.timeZones[dateManager.timeZoneSelection.primary], let tzList2 = tzList[dateManager.timeZoneSelection.secondary], tzList2.count > 0 { +#if os(macOS) + Spacer(minLength: 20) +#endif + Picker("小區", selection: $dateManager.timeZoneSelection.tertiary) { + ForEach(tzList2.nextLevel.map { $0.nodeName }, id: \.self) { tz in + Text(tz.replacingOccurrences(of: "_", with: " ")) + } + } + .lineLimit(1) + .animation(.default, value: dateManager.timeZoneSelection) + } + } + .minimumScaleFactor(0.5) +#if os(iOS) + .pickerStyle(.wheel) +#elseif os(macOS) + .pickerStyle(.menu) +#endif + } + } + .formStyle(.grouped) + .task { + dateManager.setup(watchSetting: watchSetting, watchLayout: watchLayout, chineseCalendar: chineseCalendar) + } + .navigationTitle(Text("日時", comment: "Display time settings")) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#endif + } +} + +#Preview("Datetime") { + Datetime() +} diff --git a/Shared/Setting/Documentation.swift b/Shared/Setting/Documentation.swift new file mode 100644 index 0000000..fd2aa9b --- /dev/null +++ b/Shared/Setting/Documentation.swift @@ -0,0 +1,119 @@ +// +// Documentation.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/23/23. +// + +import SwiftUI + +extension MarkdownElement { +#if os(iOS) + typealias SysFont = UIFont +#elseif os(macOS) + typealias SysFont = NSFont +#endif + + var attributeContainer: AttributeContainer { + var container = AttributeContainer() + switch self { + case .heading: + container.font = .systemFont(ofSize: SysFont.systemFontSize * 1.05) + case .paragraph: + container.font = .systemFont(ofSize: SysFont.systemFontSize / 1.05) + } + return container + } +} + +struct Documentation: View { + struct Paragraph: Identifiable { + var id = UUID() + let title: AttributedString + let body: [AttributedString] + var show: Bool = false + } + + private let parser = MarkdownParser() + @State var articles: [Paragraph] = [] + @Environment(\.watchSetting) var watchSetting + + var body: some View { + Form { + ForEach(articles) { article in + Section { + HStack { + Text(article.title) + .frame(maxWidth: .infinity, alignment: .leading) + if article.show { + Image(systemName: "chevron.up") + .foregroundStyle(Color.accentColor) + .transition(.scale) + } else { + Image(systemName: "chevron.down") + .foregroundStyle(Color.accentColor) + .transition(.scale) + } + } + .onTapGesture { + let index = articles.firstIndex { $0.id == article.id }! + withAnimation { + articles[index].show.toggle() + } + } + if article.show { + VStack(spacing: 10) { + ForEach(0..: View { + let text: Text + @Binding var value: V + let validation: ((V) -> V)? + let completion: (() -> Void)? + @State var tempValue: V + + init(text: Text, value: Binding, validation: ((V) -> V)? = nil, completion: (() -> Void)? = nil) { + self.text = text + self._value = value + self.validation = validation + self.completion = completion + self._tempValue = State(initialValue: value.wrappedValue) + } + + var body: some View { + HStack { + text + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + TextField("", value: $tempValue, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter + }()) + .autocorrectionDisabled() + .onSubmit { + if let validation = validation { + tempValue = validation(tempValue) + } + value = tempValue + if let completion = completion { + completion() + } + } + .task { + tempValue = value + } +#if os(iOS) + .padding(.vertical, 5) +#elseif os(macOS) + .frame(height: 20) +#endif + .padding(.leading, 15) +#if os(iOS) + .background(.thickMaterial, in: RoundedRectangle(cornerRadius: 10)) +#elseif os(macOS) + .background(in: RoundedRectangle(cornerRadius: 10)) +#endif + .padding(.trailing, 10) + } + } +} + +#if os(macOS) +@Observable class FontHandler { + let allFonts = NSFontManager.shared.availableFontFamilies + var textFontMembers = [String]() + var centerFontMembers = [String]() + var textFontSelection: String = "" { + didSet { + textFontMembers = populateFontMembers(for: textFontSelection) + if let first = textFontMembers.first { + textFontMemberSelection = first + } + } + } + var textFontMemberSelection: String = "" + var centerFontSelection: String = "" { + didSet { + centerFontMembers = populateFontMembers(for: centerFontSelection) + if let first = centerFontMembers.first { + centerFontMemberSelection = first + } + } + } + var centerFontMemberSelection: String = "" + + var textFont: NSFont? { + get { + readFont(family: textFontSelection, style: textFontMemberSelection) + } set { + if let font = newValue { + let (textFamily, textMember) = getFontFamilyAndMember(font: font) + if let textFamily = textFamily { + textFontSelection = textFamily + textFontMembers = populateFontMembers(for: textFamily) + textFontMemberSelection = textMember ?? textFontMembers.first ?? textFontMemberSelection + } + } + } + } + + var centerFont: NSFont? { + get { + readFont(family: centerFontSelection, style: centerFontMemberSelection) + } set { + if let font = newValue { + let (centerFamily, centerMember) = getFontFamilyAndMember(font: font) + if let centerFamily = centerFamily { + centerFontSelection = centerFamily + centerFontMembers = populateFontMembers(for: centerFamily) + centerFontMemberSelection = centerMember ?? centerFontMembers.first ?? centerFontMemberSelection + } + } + } + } + + func populateFontMembers(for fontFamily: String) -> [String] { + var allMembers = [String]() + let members = NSFontManager.shared.availableMembers(ofFontFamily: fontFamily) + for member in members ?? [[Any]]() { + if let fontType = member[1] as? String { + allMembers.append(fontType) + } + } + return allMembers + } + + func readFont(family: String, style: String) -> NSFont? { + let size = NSFont.systemFontSize + if let font = NSFont(name: "\(family.filter { !$0.isWhitespace })-\(style.filter { !$0.isWhitespace })", size: size) { + return font + } + let members = NSFontManager.shared.availableMembers(ofFontFamily: family) ?? [[Any]]() + for i in 0.. (String?, String?) { + let family = font.familyName + let member = font.fontName.split(separator: "-").last.map {String($0)} + return (family, member) + } +} +#endif + +struct LayoutSetting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting +#if os(macOS) + static let nameMapping = [ + "space": NSLocalizedString("空格", comment: "Space separator"), + "dot": NSLocalizedString("・", comment: "・"), + "none": NSLocalizedString("無", comment: "No separator") + ] + @Environment(\.chineseCalendar) var chineseCalendar + @State var fontHandler = FontHandler() +#endif + + var body: some View { + Form { +#if os(macOS) + Section(header: Text("狀態欄", comment: "Status Bar setting")) { + HStack { + Toggle("日", isOn: watchLayout.binding(\.statusBar.date)) + Spacer(minLength: 20) + Toggle("時", isOn: watchLayout.binding(\.statusBar.time)) + } + HStack { + Picker("節日", selection: watchLayout.binding(\.statusBar.holiday)) { + ForEach(0...2, id: \.self) { Text(String($0)) } + } + Spacer(minLength: 20) + Picker("讀號", selection: watchLayout.binding(\.statusBar.separator)) { + ForEach(WatchLayout.StatusBar.Separator.allCases, id: \.self) { Text(LayoutSetting.nameMapping[$0.rawValue]!) } + } + } + } + Section(header: Text("字體", comment: "Font selection")) { + HStack { + Picker("小字", selection: $fontHandler.textFontSelection) { + ForEach(fontHandler.allFonts, id:\.self) { family in + Text(family) + } + } + Picker("麤細", selection: $fontHandler.textFontMemberSelection) { + ForEach(fontHandler.textFontMembers, id:\.self) { member in + Text(member) + } + } + .labelsHidden() + } + + HStack { + Picker("大字", selection: $fontHandler.centerFontSelection) { + ForEach(fontHandler.allFonts, id:\.self) { family in + Text(family) + } + } + + Picker("麤細", selection: $fontHandler.centerFontMemberSelection) { + ForEach(fontHandler.centerFontMembers, id:\.self) { member in + Text(member) + } + } + .labelsHidden() + } + } + .onChange(of: fontHandler.textFont) { _, _ in + if let font = fontHandler.textFont { + watchLayout.textFont = font + } + } + + .onChange(of: fontHandler.centerFont) { _, _ in + if let font = fontHandler.centerFont { + watchLayout.centerFont = font + } + } +#endif +#if os(iOS) + Section(header: Text("形", comment: "Shape")) { + LayoutSettingCell(text: watchSetting.vertical ? Text("寬", comment: "Width") : Text("高", comment: "Height"), value: watchLayout.binding(\.watchSize.width)) { max(10.0, $0) } + LayoutSettingCell(text: watchSetting.vertical ? Text("高", comment: "Height") : Text("寬", comment: "Width"), value: watchLayout.binding(\.watchSize.height)) { max(10.0, $0) } + LayoutSettingCell(text: Text("圓角比例", comment: "Corner radius ratio"), value: watchLayout.binding(\.cornerRadiusRatio)) { min(1.0, max(0.0, $0)) } + LayoutSettingCell(text: Text("陰影大小", comment: "Shadow size"), value: watchLayout.binding(\.shadowSize)) { min(0.1, max(0.0, $0)) } + } + Section(header: Text("字偏", comment: "Text Shift")) { + LayoutSettingCell(text: Text("大字平移", comment: "Height"), value: watchLayout.binding(\.centerTextOffset)) + LayoutSettingCell(text: Text("大字縱移", comment: "Height"), value: watchLayout.binding(\.centerTextHOffset)) + LayoutSettingCell(text: Text("小字平移", comment: "Height"), value: watchLayout.binding(\.horizontalTextOffset)) + LayoutSettingCell(text: Text("小字縱移", comment: "Height"), value: watchLayout.binding(\.verticalTextOffset)) + } +#elseif os(macOS) + Section(header: Text("形", comment: "Shape")) { + HStack { + LayoutSettingCell(text: Text("寬", comment: "Width"), value: watchLayout.binding(\.watchSize.width)) { max(10.0, $0) } completion: { + AppDelegate.instance?.watchPanel.panelPosition() + } + LayoutSettingCell(text: Text("高", comment: "Height"), value: watchLayout.binding(\.watchSize.height)) { max(10.0, $0) } completion: { + AppDelegate.instance?.watchPanel.panelPosition() + } + } + HStack { + LayoutSettingCell(text: Text("圓角比例", comment: "Corner radius ratio"), value: watchLayout.binding(\.cornerRadiusRatio)) { min(1.0, max(0.0, $0)) } + LayoutSettingCell(text: Text("陰影大小", comment: "Shadow size"), value: watchLayout.binding(\.shadowSize)) { min(0.1, max(0.0, $0)) } + } + } + Section(header: Text("字偏", comment: "Text Shift")) { + HStack { + LayoutSettingCell(text: Text("大字平移", comment: "Height"), value: watchLayout.binding(\.centerTextOffset)) + LayoutSettingCell(text: Text("大字縱移", comment: "Height"), value: watchLayout.binding(\.centerTextHOffset)) + } + HStack { + LayoutSettingCell(text: Text("小字平移", comment: "Height"), value: watchLayout.binding(\.horizontalTextOffset)) + LayoutSettingCell(text: Text("小字縱移", comment: "Height"), value: watchLayout.binding(\.verticalTextOffset)) + } + } +#endif + } + .formStyle(.grouped) + .navigationTitle(Text("佈局", comment: "Layout settings section")) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#elseif os(macOS) + .onChange(of: watchLayout.statusBar) { _, _ in + if let delegate = AppDelegate.instance { + delegate.updateStatusBar(dateText: delegate.statusBar(from: chineseCalendar, options: watchLayout)) + } + } + .task { + fontHandler.textFont = watchLayout.textFont + fontHandler.centerFont = watchLayout.centerFont + } +#endif + } +} + +#Preview("LayoutSetting") { + LayoutSetting() +} diff --git a/Shared/Setting/Location.swift b/Shared/Setting/Location.swift new file mode 100644 index 0000000..6246909 --- /dev/null +++ b/Shared/Setting/Location.swift @@ -0,0 +1,362 @@ +// +// Location.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/24/23. +// + +import SwiftUI + +internal func coordinateDesp(coordinate: CGPoint) -> (lat: String, lon: String) { + var latitudeLabel = "" + if coordinate.x > 0 { + latitudeLabel = NSLocalizedString("北緯", comment: "N") + } else if coordinate.x < 0 { + latitudeLabel = NSLocalizedString("南緯", comment: "S") + } + let latitude = Int(round(abs(coordinate.x) * 3600)) + var latitudeString = "\(latitude / 3600)°\((latitude % 3600) / 60)\'\(latitude % 60)\"" + if Locale.isChinese { + latitudeString = "\(latitudeLabel) \(latitudeString)" + } else { + latitudeString = "\(latitudeString) \(latitudeLabel)" + } + + var longitudeLabel = "" + if coordinate.y > 0 { + longitudeLabel = NSLocalizedString("東經", comment: "E") + } else if coordinate.y < 0 { + longitudeLabel = NSLocalizedString("西經", comment: "W") + } + let longitude = Int(round(abs(coordinate.y) * 3600)) + var longitudeString = "\(longitude / 3600)°\((longitude % 3600) / 60)\'\(longitude % 60)\"" + if Locale.isChinese { + longitudeString = "\(longitudeLabel) \(longitudeString)" + } else { + longitudeString = "\(longitudeString) \(longitudeLabel)" + } + + return (latitudeString, longitudeString) +} + +struct LocationSelection: Equatable { + var positive = true + var degree: Int = 0 + var minute: Int = 0 + var second: Int = 0 + + var value: CGFloat { + var locationValue = CGFloat(degree) + locationValue += CGFloat(minute) / 60 + locationValue += CGFloat(second) / 3600 + locationValue *= positive ? 1.0 : -1.0 + return locationValue + } + + static func from(value: CGFloat) -> Self { + var values: [Int] = [0, 0, 0] + let tempValue = Int(round(abs(value) * 3600)) + values[0] = tempValue / 3600 + values[1] = (tempValue % 3600) / 60 + values[2] = tempValue % 60 + return LocationSelection(positive: value >= 0, degree: values[0], minute: values[1], second: values[2]) + } +} + +@Observable fileprivate class LocationData { + var locationManager: LocationManager? + var watchLayout: WatchLayout? + var locationUnavailable = false + + var timezoneLongitude: CGFloat { + let logitude = (CGFloat(Calendar.current.timeZone.secondsFromGMT()) - Calendar.current.timeZone.daylightSavingTimeOffset()) / 240 + return ((logitude + 180) %% 360) - 180 + } + + var locationEnabled: Bool { + get { + (locationManager?.enabled ?? false) || (watchLayout?.location != nil) + } set { + if newValue { + locationManager?.enabled = true + if !gpsEnabled { + watchLayout?.location = CGPoint(x: 0.0, y: timezoneLongitude) + } + } else { + locationManager?.enabled = false + watchLayout?.location = nil + } + } + } + + var gpsEnabled: Bool { + get { + locationManager?.enabled ?? false + } set { + if newValue { + locationManager?.enabled = true + if !gpsEnabled { + locationUnavailable = true + } + } else { + locationManager?.enabled = false + watchLayout?.location = watchLayout?.location ?? locationManager?.location ?? CGPoint(x: 0.0, y: timezoneLongitude) + } + } + } + + var gpsLocation: CGPoint? { + locationManager?.location + } + + var manualLocation: CGPoint? { + watchLayout?.location + } + + var latitudeSelection: LocationSelection { + get { + LocationSelection.from(value: manualLocation?.x ?? 0) + } set { + watchLayout?.location?.x = newValue.value + } + } + + var longitudeSelection: LocationSelection { + get { + LocationSelection.from(value: manualLocation?.y ?? CGFloat(Calendar.current.timeZone.secondsFromGMT()) / 240) + } set { + watchLayout?.location?.y = newValue.value + } + } + + var location: CGPoint? { + self.gpsLocation ?? self.manualLocation + } + + func setup(locationManager: LocationManager, watchLayout: WatchLayout) { + self.locationManager = locationManager + self.watchLayout = watchLayout + } +} + +#if os(macOS) +struct OnSubmitTextField: View { + let title: LocalizedStringKey + let formatter: NumberFormatter + @Binding var value: V + @State var tempValue: V + + init(_ title: LocalizedStringKey, value: Binding, formatter: NumberFormatter) { + self.title = title + self.formatter = formatter + self._value = value + self._tempValue = State(initialValue: value.wrappedValue) + } + + var body: some View { + TextField(title, value: $tempValue, formatter: formatter) + .onSubmit { + value = tempValue + } + } +} +#endif + +struct Location: View { + @State fileprivate var locationData = LocationData() + @Environment(\.watchSetting) var watchSetting + @Environment(\.locationManager) var locationManager + @Environment(\.watchLayout) var watchLayout + @Environment(\.chineseCalendar) var chineseCalendar + + var body: some View { + Form { + Section { + Toggle("定位", isOn: $locationData.locationEnabled) + Toggle("今地", isOn: $locationData.gpsEnabled) + .disabled(!locationData.locationEnabled) + } + .alert("迷蹤難尋", isPresented: $locationData.locationUnavailable) { + Button("罷", role: .cancel) {} + } message: { + Text("未開啓定位。如欲使用 GPS 定位,請於設置中啓用", comment: "Please enable location service in Settings App") + } + if locationData.locationEnabled { + if locationData.gpsEnabled { + Section(header: Text("經緯度", comment: "Geo Location section")) { + if let location = locationManager.location { + let locationString = coordinateDesp(coordinate: location) + Text("\(locationString.0), \(locationString.1)") + .frame(maxWidth: .infinity, alignment: .center) + .privacySensitive() + } else { + Text("虚無", comment: "Location fails to load") + .frame(maxWidth: .infinity, alignment: .center) + } + } + } else { + let manualLocationDesp = locationData.manualLocation.map { coordinateDesp(coordinate: $0) } + let latitudeTitle = if let desp = manualLocationDesp { + NSLocalizedString("緯度:", comment: "Latitude section") + desp.0 + } else { + NSLocalizedString("緯度", comment: "Latitude section") + } + Section(header: Text(latitudeTitle)) { + HStack { +#if os(iOS) + Picker("度", selection: $locationData.latitudeSelection.degree) { + ForEach(0...89, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.latitudeSelection) + Picker("分", selection: $locationData.latitudeSelection.minute) { + ForEach(0...59, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.latitudeSelection) + Picker("秒", selection: $locationData.latitudeSelection.second) { + ForEach(0...60, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.latitudeSelection) +#elseif os(macOS) + OnSubmitTextField("度", value: $locationData.latitudeSelection.degree, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 89 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) + OnSubmitTextField("分", value: $locationData.latitudeSelection.minute, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 59 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) + OnSubmitTextField("秒", value: $locationData.latitudeSelection.second, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 60 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) +#endif + Picker("北南", selection: $locationData.latitudeSelection.positive) { + ForEach([true, false], id: \.self) { value in + Text(value ? NSLocalizedString("北", comment: "N in geo location") : NSLocalizedString("南", comment: "S in geo location")) + } + } + .animation(.default, value: locationData.latitudeSelection) + } +#if os(iOS) + .pickerStyle(.wheel) +#elseif os(macOS) + .pickerStyle(.menu) + .frame(height: 20) +#endif + } + + let longitudeTitle = if let desp = manualLocationDesp { + NSLocalizedString("經度:", comment: "Longitude section") + desp.1 + } else { + NSLocalizedString("經度", comment: "Longitude section") + } + Section(header: Text(longitudeTitle)) { + HStack { +#if os(iOS) + Picker("度", selection: $locationData.longitudeSelection.degree) { + ForEach(0...179, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.longitudeSelection) + Picker("分", selection: $locationData.longitudeSelection.minute) { + ForEach(0...59, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.longitudeSelection) + Picker("秒", selection: $locationData.longitudeSelection.second) { + ForEach(0...60, id: \.self) { value in + Text("\(value)") + } + } + .animation(.default, value: locationData.longitudeSelection) +#elseif os(macOS) + OnSubmitTextField("度", value: $locationData.longitudeSelection.degree, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 179 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) + OnSubmitTextField("分", value: $locationData.longitudeSelection.minute, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 59 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) + OnSubmitTextField("秒", value: $locationData.longitudeSelection.second, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.maximum = 60 + formatter.minimum = 0 + return formatter + }()) + .frame(height: 20) + .background(in: RoundedRectangle(cornerRadius: 10)) +#endif + Picker("東西", selection: $locationData.longitudeSelection.positive) { + ForEach([true, false], id: \.self) { value in + Text(value ? NSLocalizedString("東", comment: "E in geo location") : NSLocalizedString("西", comment: "W in geo location")) + } + } + .animation(.default, value: locationData.longitudeSelection) + } +#if os(iOS) + .pickerStyle(.wheel) +#elseif os(macOS) + .pickerStyle(.menu) +#endif + } + } + } + } + .formStyle(.grouped) + .task { + locationData.setup(locationManager: locationManager, watchLayout: watchLayout) + } + .onChange(of: locationData.location) { _, newValue in + chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, location: newValue) + } + .navigationTitle(Text("經緯度", comment: "Geo Location section")) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#endif + } +} + +#Preview("Location") { + Location() +} diff --git a/Shared/Setting/RingSetting.swift b/Shared/Setting/RingSetting.swift new file mode 100644 index 0000000..6799570 --- /dev/null +++ b/Shared/Setting/RingSetting.swift @@ -0,0 +1,480 @@ +// +// SliderView.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/23/23. +// + +import SwiftUI + +#if os(macOS) +@MainActor +class ColorPanelObserver { + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(colorPanelWillClose(notification:)), + name: NSWindow.willCloseNotification, + object: nil + ) + } + + @objc private func colorPanelWillClose(notification: Notification) { + guard let closingWindow = notification.object as? NSWindow, + closingWindow == NSColorPanel.shared else { + return + } + + NSColorPanel.shared.setTarget(nil) + NSColorPanel.shared.setAction(nil) + } +} + +class ColorNode: NSControl, NSColorChanging { + private let callBack: (NSColor) -> Void + var color: NSColor { + didSet { + if let layer = self.layer as? CAShapeLayer { + layer.fillColor = color.cgColor + } + } + } + + init(frame frameRect: NSRect, color: NSColor, action: @escaping (NSColor) -> Void) { + self.color = color + self.callBack = action + super.init(frame: frameRect) + self.wantsLayer = true + let colorLayer = CAShapeLayer() + colorLayer.path = CGPath(ellipseIn: frameRect, transform: nil) + colorLayer.fillColor = color.cgColor + self.layer = colorLayer + let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(onTap(sender:))) + self.addGestureRecognizer(clickGesture) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func onTap(sender: NSClickGestureRecognizer) { + let colorPanel = NSColorPanel.shared + colorPanel.setTarget(nil) + colorPanel.setAction(nil) + var position = convert(NSPoint(x: bounds.midX, y: bounds.midY), to: nil) + position = window?.convertPoint(toScreen: position) ?? position + position.x -= colorPanel.frame.width / 2 + position.y -= colorPanel.frame.height / 2 + colorPanel.setFrameOrigin(position) + colorPanel.mode = .RGB + colorPanel.showsAlpha = true + colorPanel.colorSpace = .displayP3 + colorPanel.color = color + colorPanel.orderFrontRegardless() + colorPanel.setTarget(self) + colorPanel.setAction(#selector(changeColor(_:))) + } + + @objc func changeColor(_ sender: NSColorPanel?) { + if let newColor = sender?.color { + color = newColor + callBack(newColor) + } + } +} + +struct ColorNodeView: NSViewRepresentable { + let size: CGSize + @Binding var color: CGColor + + func makeNSView(context: Context) -> ColorNode { + return ColorNode(frame: NSRect(origin: .zero, size: size), color: NSColor(cgColor: color)!, action: { color = $0.cgColor }) + } + + func updateNSView(_ nsView: ColorNode, context: Context) { + nsView.color = NSColor(cgColor: color)! + } +} +#endif + +@Observable final class ViewGradient { + private var colors: [CGColor] = [] + private var values: [CGFloat] = [] + var isLoop: Bool = false + + var gradientStops: [Gradient.Stop] { + var stops = [Gradient.Stop]() + for (value, color) in zip(values, colors) { + let stop = Gradient.Stop(color: Color(cgColor: color), location: value) + stops.append(stop) + } + stops.sort { $0.location < $1.location } + if isLoop, let firstStop = stops.first { + var lastStop = firstStop + lastStop.location = 1.0 + stops.append(lastStop) + } + return stops + } + + var count: Int { + colors.count + } + + func bindColor(at index: Int) -> Binding { + Binding(get: { + self.color(at: index) + }, set: { newValue in + if index >= 0 && index < self.colors.count { + self.colors[index] = newValue + } + }) + } + + func color(at index: Int) -> CGColor { + if index >= 0 && index < self.colors.count { + self.colors[index] + } else { + CGColor(gray: 0, alpha: 0) + } + } + + func value(at index: Int) -> CGFloat { + if index >= 0 && index < self.values.count { + self.values[index] + } else { + 0 + } + } + + func updateValue(at index: Int, with newValue: CGFloat) { + if index >= 0 && index < self.values.count { + self.values[index] = newValue + } + } + + func add(color: CGColor, at value: CGFloat) { + let index = values.insertionIndex(of: value, comparison: { $0 < $1 }) + colors.insert(color, at: index) + values.insert(value, at: index) + } + + func remove(at index: Int) { + if index >= 0 && index < colors.count { + colors.remove(at: index) + values.remove(at: index) + } + } + + func export(allowLoop: Bool = true) -> WatchLayout.Gradient { + return WatchLayout.Gradient(locations: values, colors: colors, loop: allowLoop && isLoop) + } + + init() {} + + init(from gradient: WatchLayout.Gradient, allowLoop: Bool = true) { + isLoop = allowLoop && gradient.isLoop + if isLoop { + colors = gradient.colors.dropLast() + values = gradient.locations.dropLast() + } else { + colors = gradient.colors + values = gradient.locations + } + } +} + +struct GradientSliderView: View { + let barHeight: CGFloat = 5 +#if os(iOS) + let slideHeight: CGFloat = 18 + let pickerSize: CGFloat = 14 +#elseif os(macOS) + let slideHeight: CGFloat = 10 + let pickerSize: CGFloat = 10 +#endif + let padding: CGFloat = 0 + let text: Text + @Binding var gradient: WatchLayout.Gradient + let allowLoop: Bool + + @State private var viewGradient = ViewGradient() + @State private var position: (index: Int, pos: CGPoint)? = nil + + var body: some View { + GeometryReader { proxy in + let size: CGSize = proxy.size + + let tapGesture = SpatialTapGesture() + .onEnded { value in + var newPosition = valueForPosition(value.location, in: size) + newPosition = max(0, min(1, newPosition)) + let interpolateColor = gradient.interpolate(at: newPosition) + viewGradient.add(color: interpolateColor, at: newPosition) + } + + VStack { + HStack { + text + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + Spacer() + if allowLoop { + Text("廻環", comment: "Color Gradient is Loop") + .lineLimit(1) + .frame(alignment: .trailing) + .padding(.leading, 10) + Toggle("", isOn: $viewGradient.isLoop) + .labelsHidden() + .onChange(of: viewGradient.isLoop) { _, _ in + gradient = viewGradient.export(allowLoop: allowLoop) + } + } + } + ZStack { + + // Gradient background + LinearGradient(gradient: Gradient(stops: viewGradient.gradientStops), startPoint: .leading, endPoint: .trailing) + .frame(height: barHeight) + .cornerRadius(size.height / 2) + .position(x: size.width / 2 - padding, y: barHeight / 2) + .padding(.top, slideHeight - barHeight / 2) + .gesture(tapGesture) + + ForEach(0.. 2 && abs(value.location.y - pickerSize) > slideHeight * 2 { +#if os(iOS) + if position == nil { + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + } +#endif + let boundedPos = boundByView(point: value.location, bound: size) + position = (index: index, pos: boundedPos) + } else { + viewGradient.updateValue(at: index, with: valueForPosition(value.location, in: size)) +#if os(iOS) + if position != nil { + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + } +#endif + position = nil + } + } + .onEnded { value in + if viewGradient.count > 2 && abs(value.location.y - pickerSize) > slideHeight * 2 { + viewGradient.remove(at: index) + position = nil + } + gradient = viewGradient.export(allowLoop: allowLoop) + } + + let targetPos = if let target = position, target.index == index { + target.pos + } else { + positionForValue(viewGradient.value(at: index), in: size) + } + let removing = if let target = position { + target.index == index + } else { + false + } +#if os(iOS) + ColorPicker("", selection: viewGradient.bindColor(at: index)) + .labelsHidden() + .shadow(color: .black.opacity(0.15), radius: 6, x: -3, y: 4) + .opacity(removing ? 0.3 : 1.0) + .onChange(of: viewGradient.color(at: index)) { _, _ in + gradient = viewGradient.export(allowLoop: allowLoop) + } + .frame(width: pickerSize * 2, height: pickerSize * 2) + .position(targetPos) + .gesture(dragGesture) +#elseif os(macOS) + ColorNodeView(size: CGSize(width: pickerSize * 2, height: pickerSize * 2), color: viewGradient.bindColor(at: index)) + .shadow(color: .black.opacity(0.3), radius: 2, x: -1, y: 1) + .opacity(removing ? 0.3 : 1.0) + .onChange(of: viewGradient.color(at: index)) { _, _ in + gradient = viewGradient.export(allowLoop: allowLoop) + } + .frame(width: pickerSize * 2, height: pickerSize * 2) + .position(targetPos) + .gesture(dragGesture) +#endif + } + } + .task { + viewGradient = ViewGradient(from: gradient, allowLoop: allowLoop) + } + .contentShape(Rectangle()) + .padding(.horizontal, padding) + } + } + } + + private func boundByView(point: CGPoint, bound: CGSize) -> CGPoint { + var boundedPoint = point +#if os(iOS) + let verticalOffset = -pickerSize + barHeight - 33 +#elseif os(macOS) + let verticalOffset = -pickerSize + barHeight - 18 +#endif + boundedPoint.x = max(min(boundedPoint.x, bound.width - padding * 2), 0) + boundedPoint.y = max(min(boundedPoint.y, bound.height + verticalOffset), verticalOffset) + return boundedPoint + } + + private func positionForValue(_ value: CGFloat, in size: CGSize) -> CGPoint { + return CGPoint(x: (size.width - (pickerSize + padding) * 2) * value + pickerSize, y: slideHeight) + } + + private func valueForPosition(_ position: CGPoint, in size: CGSize) -> CGFloat { + let value = (position.x - pickerSize) / (size.width - (pickerSize + padding) * 2) + return max(0.0, min(1.0, value)) + } +} + +struct SliderView: View { + @Binding var value: CGFloat + @State var currentValue: CGFloat = 0 + let label: Text + + var body: some View { +#if os(iOS) + VStack { + HStack { + label + TextField("", value: $currentValue, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter + }()) + .labelsHidden() + .disabled(true) + .multilineTextAlignment(.trailing) + } + Slider(value: $currentValue, in: 0.0...1.0) { editing in + if !editing { + value = currentValue + } + } + .labelsHidden() + } + .onAppear { + currentValue = value + } +#elseif os(macOS) + HStack { + label + .frame(maxWidth: 150, alignment: .leading) + Slider(value: $currentValue, in: 0.0...1.0) { editing in + if !editing { + value = currentValue + } + } + .labelsHidden() + TextField("", value: $currentValue, formatter: { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 0 + return formatter + }()) + .frame(maxWidth: 40) + .labelsHidden() + .disabled(true) + .multilineTextAlignment(.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) + Text("暗", comment: "dark theme") + .lineLimit(1) + .frame(alignment: .trailing) + .padding(.horizontal, 5) + ColorPicker("", selection: $darkColor) + .labelsHidden() + + } + } +} + +@MainActor +struct RingSetting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting +#if os(macOS) + let observer = ColorPanelObserver() +#endif + + var body: some View { + Form { + Section(header: Text("漸變色", comment: "Gradient Pickers")) { +#if os(iOS) + let height = 80.0 +#else + let height = 45.0 +#endif + GradientSliderView(text: Text("年輪", comment: "Year Ring Gradient"), gradient: watchLayout.binding(\.firstRing), allowLoop: true) + .frame(height: height) + GradientSliderView(text: Text("月輪", comment: "Month Ring Gradient"), gradient: watchLayout.binding(\.secondRing), allowLoop: true) + .frame(height: height) + GradientSliderView(text: Text("日輪", comment: "Day Ring Gradient"), gradient: watchLayout.binding(\.thirdRing), allowLoop: true) + .frame(height: height) + GradientSliderView(text: Text("大字", comment: "Day Ring Gradient"), gradient: watchLayout.binding(\.centerFontColor), allowLoop: false) + .frame(height: height) + } + Section(header: Text("透明度", comment: "Transparency sliders")) { + SliderView(value: watchLayout.binding(\.shadeAlpha), label: Text("殘圈透明", comment: "Inactive ring opacity")) + SliderView(value: watchLayout.binding(\.majorTickAlpha), label: Text("大刻透明", comment: "Major Tick opacity")) + SliderView(value: watchLayout.binding(\.minorTickAlpha), label: Text("小刻透明", comment: "Minor Tick opacity")) + } + Section(header: Text("明暗主題色", comment: "Watch face colors in light and dark themes")) { + 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)) + } + } + .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("Ring Setting") { + RingSetting() +} diff --git a/Shared/Setting/ThemesList.swift b/Shared/Setting/ThemesList.swift new file mode 100644 index 0000000..2b65673 --- /dev/null +++ b/Shared/Setting/ThemesList.swift @@ -0,0 +1,359 @@ +// +// SwiftUIView.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/16/23. +// + +import SwiftUI +import SwiftData + +private func loadThemes(data: [ThemeData]) -> [String: [ThemeData]] { + var newThemes = [String: [ThemeData]]() + for data in data { + if !data.isNil { + if newThemes[data.deviceName!] == nil { + newThemes[data.deviceName!] = [data] + } else { + newThemes[data.deviceName!]!.append(data) + } + } + } + for deviceName in newThemes.keys { + newThemes[deviceName]!.sort { $0.modifiedDate! > $1.modifiedDate! } + } + return newThemes +} + +struct ThemesList: View { + @Query private var dataStack: [ThemeData] + @Environment(\.modelContext) private var modelContext + @Environment(\.watchSetting) var watchSetting + @Environment(\.watchLayout) var watchLayout + + @State private var renameAlert = false + @State private var createAlert = false + @State private var switchAlert = false + @State private var deleteAlert = false + @State private var newName = "" + @State private var target: ThemeData? = nil + private var invalidName: Bool { + let diviceName = target?.deviceName ?? currentDeviceName + return !validateName(newName, onDevice: diviceName) + } + private var themes: [String: [ThemeData]] { + loadThemes(data: dataStack) + } + let currentDeviceName = ThemeData.deviceName + + var body: some View { + let newTheme = Button { + newName = validName(ThemeData.defaultName) + createAlert = true + } label: { + Label(NSLocalizedString("謄錄", comment: "Save current layout button"), systemImage: "plus") + .frame(maxWidth: .infinity, alignment: .center) + .foregroundStyle(Color.green) + } + + let newThemeConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { + let newTheme = ThemeData(name: newName, code: WatchLayout.shared.encode()) + modelContext.insert(newTheme) + do { + try modelContext.save() + } catch { + print("Save failed. \(error.localizedDescription)") + } + } + + let renameConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { + if let target = target, !target.isNil { + target.name = newName + do { + try modelContext.save() + } catch { + print("Save failed. \(error.localizedDescription)") + } + self.target = nil + } + } +#if os(macOS) + let readButton = Button { + readFile(context: modelContext) + do { + try modelContext.save() + } catch { + print("Save failed. \(error.localizedDescription)") + } + } label: { + Label(NSLocalizedString("讀入", comment: "Load from file button"), systemImage: "square.and.arrow.down") + } +#endif + + Form { +#if os(iOS) + Section { + newTheme + } +#endif + let deviceNames = themes.keys.sorted(by: {$0 > $1}).sorted(by: {prev, _ in prev == currentDeviceName}) + ForEach(deviceNames, id: \.self) { key in + Section(key) { + ForEach(themes[key]!, id: \.self) { theme in + if !theme.isNil { + + let deleteButton = Button { + target = theme + deleteAlert = true + } label: { + Label(NSLocalizedString("刪", comment: "Delete action"), 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") + } + .tint(Color.indigo) + +#if os(macOS) + let saveButton = Button { + writeFile(theme: theme) + } label: { + Label(NSLocalizedString("寫下", comment: "Save to file button"), systemImage: "square.and.arrow.up") + } +#endif + + HStack { + Button { + target = theme + switchAlert = true + } label: { + Text(theme.name!) + } +#if os(iOS) + .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) + .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) + saveButton + .labelStyle(.titleAndIcon) + } +#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) + Menu { + renameButton + deleteButton + saveButton + } label: { + Image(systemName: "ellipsis") + } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.borderless) + .labelStyle(.titleAndIcon) +#endif + } + } + } + } + } + } + .formStyle(.grouped) + .alert(NSLocalizedString("更名", comment: "Rename action"), isPresented: $renameAlert) { + TextField("", text: $newName) + .labelsHidden() + renameConfirm + .disabled(invalidName) + Button(NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), role: .cancel) { target = nil } + } message: { + Text("不得爲空,不得重名", comment: "no blank, no duplicate name") + } + .alert(NSLocalizedString("取名", comment: "set a name"), isPresented: $createAlert) { + TextField("", text: $newName) + .labelsHidden() + newThemeConfirm + .disabled(invalidName) + Button(NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), role: .cancel) {} + } message: { + Text("不得爲空,不得重名", comment: "no blank, no duplicate name") + } + .alert((target != nil && !target!.isNil) ? (NSLocalizedString("換爲:", comment: "Confirm to select theme message") + target!.name!) : NSLocalizedString("換不得", comment: "Cannot switch theme"), isPresented: $switchAlert) { + Button(NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), role: .cancel) { target = nil } + Button(NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings"), role: .destructive) { + if let target = target, !target.isNil { + watchLayout.update(from: target.code!) +#if os(iOS) + let _ = WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) +#elseif os(macOS) + if let delegate = AppDelegate.instance { + delegate.update() + delegate.watchPanel.panelPosition() + } +#endif + self.target = nil + } + } + } + .alert((target != nil && !target!.isNil) ? (NSLocalizedString("刪:", comment: "Confirm to delete theme message") + target!.name!) : NSLocalizedString("刪不得", comment: "Cannot switch 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 { + modelContext.delete(target) + do { + try modelContext.save() + } catch { + print("Save failed. \(error.localizedDescription)") + } + } + } + } + .navigationTitle(Text("主題庫", comment: "manage saved themes")) +#if os(macOS) + .toolbar { + Menu { + newTheme + readButton + } label: { + Image(systemName: "ellipsis") + } + .menuIndicator(.hidden) + .menuStyle(.button) + .buttonStyle(.borderless) + .labelStyle(.titleAndIcon) + } +#elseif os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } +#endif + } + + func validateName(_ name: String, onDevice deviceName: String) -> Bool { + if name.count > 0 { + let currentDeviceThemes = themes[deviceName] + return currentDeviceThemes == nil || !(currentDeviceThemes!.map { $0.name }.contains(name)) + } else { + return true + } + } + + func validName(_ name: String, device: String? = nil) -> String { + let namePattern = /^(.*) (\d+)$/ + let baseName: String + var i: Int + if let match = try? namePattern.firstMatch(in: name) { + baseName = String(match.output.1) + i = Int(match.output.2)! + } else { + baseName = name + i = 2 + } + while !validateName("\(baseName) \(i)", onDevice: device ?? currentDeviceName) { + i += 1 + } + return "\(baseName) \(i)" + } + +#if os(macOS) + @MainActor + func writeFile(theme: ThemeData) { + guard !theme.isNil else { return } + let panel = NSSavePanel() + panel.level = NSWindow.Level.floating + panel.title = NSLocalizedString("以筆書之", comment: "Save File title") + panel.allowedContentTypes = [.text] + panel.canCreateDirectories = true + panel.isExtensionHidden = false + panel.allowsOtherFileTypes = false + panel.message = NSLocalizedString("將主題書於紙上", comment: "Save File message") + panel.nameFieldLabel = NSLocalizedString("題名", comment: "File name prompt") + panel.nameFieldStringValue = "\(theme.name!).txt" + panel.begin { result in + if result == .OK, let file = panel.url { + do { + try theme.code!.data(using: .utf8)?.write(to: file, options: .atomicWrite) + } catch { + let alert = NSAlert() + alert.messageText = NSLocalizedString("寫不出", comment: "Save Failed") + alert.informativeText = error.localizedDescription + alert.alertStyle = .critical + alert.beginSheetModal(for: panel) + } + } + } + } + + @MainActor + func readFile(context: ModelContext) { + let panel = NSOpenPanel() + panel.level = NSWindow.Level.floating + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + panel.allowedContentTypes = [.text] + panel.title = NSLocalizedString("讀入主題", comment: "Open File title") + panel.message = NSLocalizedString("選一卷主題讀入", comment: "Open File message") + panel.begin { result in + if result == .OK, let file = panel.url { + do { + 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) + context.insert(theme) + try modelContext.save() + } catch { + let alert = NSAlert() + alert.messageText = NSLocalizedString("讀不入", comment: "Load Failed") + alert.informativeText = error.localizedDescription + alert.alertStyle = .critical + alert.beginSheetModal(for: panel) + } + } + } + } +#endif +} + +#Preview("Themes") { + ThemesList() +} diff --git a/Shared/Utilities.swift b/Shared/Utilities.swift index 9ae92c1..a9bfce1 100644 --- a/Shared/Utilities.swift +++ b/Shared/Utilities.swift @@ -7,93 +7,50 @@ import Foundation -extension Locale { - static var isChinese: Bool { - let languages = Locale.preferredLanguages - var isChinese = true - for language in languages { - let flag = language[language.startIndex.. [MarkdownElement] { var elements: [MarkdownElement] = [] - var lines = markdownString.components(separatedBy: .newlines) - var currentParagraph: String? - - while !lines.isEmpty { - let line = lines.removeFirst() - - if let headingLevel = headingLevel(for: line) { - if let paragraph = currentParagraph { - elements.append(.paragraph(text: paragraph)) - currentParagraph = nil - } - elements.append(.heading(level: headingLevel, text: headingText(for: line, with: headingLevel))) - } else { - if currentParagraph == nil { - currentParagraph = line + var currentParagraph: [AttributedString] = [] + let scanner = Scanner(string: markdownString) + while !scanner.isAtEnd { + if let line = scanner.scanUpToCharacters(from: .newlines), let attrLine = try? AttributedString(markdown: line) { + if headingLevel(for: line) > 0 { + if !currentParagraph.isEmpty { + elements.append(.paragraph(currentParagraph)) + currentParagraph = [] + } + elements.append(.heading(attrLine)) } else { - currentParagraph?.append("\n\(line)") + currentParagraph.append(attrLine) } } } - if let paragraph = currentParagraph { - elements.append(.paragraph(text: paragraph)) + if !currentParagraph.isEmpty { + elements.append(.paragraph(currentParagraph)) } return elements } - private func headingLevel(for line: String) -> Int? { + private func headingLevel(for line: String) -> Int { let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) - guard let firstCharacter = trimmedLine.first else { return nil } - guard firstCharacter == "#" else { return nil } - + guard trimmedLine.count > 0 else { return 0 } + var index = trimmedLine.startIndex var count = 0 - for char in trimmedLine { - if char == "#" { - count += 1 - } else { - break - } + while index < trimmedLine.endIndex && trimmedLine[index] == "#" { + count += 1 + index = trimmedLine.index(after: index) } - return min(count, 6) } - - private func headingText(for line: String, with level: Int) -> String { - let startIndex = line.index(line.startIndex, offsetBy: level) - let trimmedLine = line[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedLine - } } extension String { @@ -122,6 +79,12 @@ final class DataTree: CustomStringConvertible { registry = [:] } + var nextLevel: [DataTree] { + get { + offsprings + } + } + func add(element: String) -> DataTree { let data: DataTree if let index = registry[element] { diff --git a/Shared/Views/HoverView.swift b/Shared/Views/HoverView.swift new file mode 100644 index 0000000..c888389 --- /dev/null +++ b/Shared/Views/HoverView.swift @@ -0,0 +1,127 @@ +// +// NamedEntity.swift +// Chinese Time +// +// Created by Leo Liu on 6/26/23. +// + +import SwiftUI +import Observation + +@MainActor +@Observable class EntitySelection { + @ObservationIgnored var entityNotes = EntityNotes() + @ObservationIgnored var timer: Timer? = nil + @ObservationIgnored var _activeNote: [EntityNotes.EntityNote] = [] { + didSet { + timer?.invalidate() + if _activeNote.count > 0 { + timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in + Task { @MainActor in + self.activeNote = [] + } + } + } else { + timer = nil + } + } + } + var activeNote: [EntityNotes.EntityNote] { + get { + self.access(keyPath: \.activeNote) + return _activeNote + } set { + self.withMutation(keyPath: \.activeNote) { + _activeNote = newValue + } + } + } +} + +private func edgeSafePos(pos: CGPoint, bounds: CGRect, screen: CGSize) -> CGPoint { + var idealPos = pos + let boundSize = CGPoint(x: bounds.width / 2, y: bounds.height / 2) + if idealPos.x < boundSize.x { + idealPos.x = boundSize.x + } else if idealPos.x + boundSize.x > screen.width { + idealPos.x = screen.width - boundSize.x + } + if idealPos.y < boundSize.y { + idealPos.y = boundSize.y + } else if idealPos.y + boundSize.y > screen.height { + idealPos.y = screen.height - boundSize.y + } + return idealPos +} + +struct Hover: View { + @Environment(\.watchLayout) var watchLayout + @State var entityPresenting: EntitySelection + @Binding var bounds: CGRect + @Binding var tapPos: CGPoint? + @State var isChinese = Locale.isChinese + @State var prepared = true + + var body: some View { + GeometryReader { proxy in + if let tapPos = tapPos, entityPresenting.activeNote.count > 0 { + let idealPos = edgeSafePos(pos: tapPos, bounds: bounds, screen: proxy.size) + if isChinese { + HStack(alignment: .top) { + ForEach(entityPresenting.activeNote) {note in + VStack(spacing: 0) { + RoundedRectangle(cornerRadius: watchLayout.textFont.pointSize * 0.3) + .frame(width: watchLayout.textFont.pointSize, height: watchLayout.textFont.pointSize) + .foregroundStyle(Color(cgColor: note.color)) + .padding(.vertical, 1) + Spacer(minLength: 3) + .frame(maxHeight: 3) + ForEach(Array(note.name), id: \.self) { char in + Text(String(char)) + .font(.system(size: watchLayout.textFont.pointSize)) + .padding(0) + } + } + } + } + .padding(5) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 7, style: .continuous)) + .anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { proxy[$0] } + .onPreferenceChange(BoundsPreferenceKey.self) { bounds = $0 } + .position(idealPos) + } else { + VStack(alignment: .leading) { + ForEach(entityPresenting.activeNote) {note in + HStack(spacing: 0) { + RoundedRectangle(cornerRadius: watchLayout.textFont.pointSize * 0.3) + .frame(width: watchLayout.textFont.pointSize, height: watchLayout.textFont.pointSize) + .foregroundStyle(Color(cgColor: note.color)) + .padding(.horizontal, 1) + .padding(.vertical, 0) + Spacer(minLength: 3) + .frame(maxWidth: 3) + Text(Locale.translation[note.name] ?? "") + .font(.system(size: watchLayout.textFont.pointSize)) + .padding(0) + } + } + } + .padding(5) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 7, style: .continuous)) + .anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { proxy[$0] } + .onPreferenceChange(BoundsPreferenceKey.self) { bounds = $0 } + .position(idealPos) + } + } + } + } +} + +private struct BoundsPreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } + +} diff --git a/Shared/RoundedRect.swift b/Shared/Views/RoundedRect.swift similarity index 89% rename from Shared/RoundedRect.swift rename to Shared/Views/RoundedRect.swift index 0e48061..7dd2604 100644 --- a/Shared/RoundedRect.swift +++ b/Shared/Views/RoundedRect.swift @@ -100,11 +100,16 @@ final class RoundedRect { } struct OrientedPoint { + var name: String var position: CGPoint var direction: CGFloat } func arcPoints(lambdas: [CGFloat]) -> [OrientedPoint] { + return arcPoints(lambdas: lambdas.map { ChineseCalendar.NamedPosition(name: "", pos: Double($0)) }) + } + + func arcPoints(lambdas: [T]) -> [OrientedPoint] { let arcLength = bezierLength(t: 0.5) * 2 let innerWidth = _boundBox.width - 2 * _nodePos let innerHeight = _boundBox.height - 2 * _nodePos @@ -148,7 +153,8 @@ final class RoundedRect { var firstArc = [(CGFloat, Int)](), secondArc = [(CGFloat, Int)](), thirdArc = [(CGFloat, Int)](), fourthArc = [(CGFloat, Int)]() var i = 0 - for lambda in lambdas { + for lambdaPoint in lambdas { + let lambda = lambdaPoint.pos switch lambda * totalLength { case 0.0.. CGMutablePath { 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.truncatingRemainder(dividingBy: 1.0), (startingAngle+(startingAngle >= 0 ? angle : -angle)).truncatingRemainder(dividingBy: 1.0)]) + 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 path = CGMutablePath() diff --git a/Watch/SwiftUIUtilities.swift b/Shared/Views/SwiftUIUtilities.swift similarity index 100% rename from Watch/SwiftUIUtilities.swift rename to Shared/Views/SwiftUIUtilities.swift diff --git a/Shared/Views/WatchFaceBasics.swift b/Shared/Views/WatchFaceBasics.swift new file mode 100644 index 0000000..dff8f93 --- /dev/null +++ b/Shared/Views/WatchFaceBasics.swift @@ -0,0 +1,476 @@ +// +// WatchFace.swift +// ChineseTime Watch App +// +// Created by Leo Liu on 5/4/23. +// + +import SwiftUI + +private func changePhase(phase: CGFloat, angle: CGFloat) -> CGFloat { + if phase >= 0 { + return (angle + phase) % 1.0 + } else { + return (-angle + phase) % 1.0 + } +} + +class EntityNotes { + struct EntityNote: Hashable, Identifiable { + var name: String + let position: CGPoint + let color: CGColor + let id = UUID() + + func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + } + + var entities = Set() + + func reset() { + entities = Set() + } +} + +struct WatchFont { + #if os(macOS) + var font: NSFont + init(_ font: NSFont) { + self.font = font + } + #else + var font: UIFont + init(_ font: UIFont) { + self.font = font + } + #endif +} + +struct ZeroRing: View { + static let width: CGFloat = 0.05 + let shortEdge: CGFloat + let oddColor: CGColor + let evenColor: CGColor + let ringBoundPath: CGPath + let oddTicksPath: CGPath + let evenTicksPath: CGPath + fileprivate let drawableTexts: [DrawableText] + + init(width: CGFloat, viewSize: CGSize, compact: Bool, textFont: WatchFont, outerRing: RoundedRect, startingAngle: CGFloat, oddTicks: [CGFloat], evenTicks: [CGFloat], oddColor: CGColor, evenColor: CGColor, oddTexts: [String], evenTexts: [String], offset: CGSize = .zero) { + self.shortEdge = min(viewSize.width, viewSize.height) + let longEdge = max(viewSize.width, viewSize.height) + self.oddColor = oddColor + self.evenColor = evenColor + + let textRing = outerRing.shrink(by: (width + 0.003)/2 * shortEdge) + let ringBoundPath = outerRing.path + self.ringBoundPath = ringBoundPath + self.oddTicksPath = outerRing.arcPosition(lambdas: changePhase(phase: startingAngle, angles: oddTicks), width: 0.15 * shortEdge) + self.evenTicksPath = outerRing.arcPosition(lambdas: changePhase(phase: startingAngle, angles: evenTicks), width: 0.15 * shortEdge) + + let fontSize: CGFloat = min(shortEdge * 0.03, longEdge * 0.025) * (compact ? 1.5 : 1.0) + let font = textFont.font.withSize(fontSize) + var drawableTexts = [DrawableText]() + + let oddPoints = textRing.arcPoints(lambdas: oddTicks.map { changePhase(phase: startingAngle, angle: $0) }) + for i in 0..= 0 && $0.pos < 1 }.map { ChineseCalendar.NamedPosition(name: $0.name, pos: changePhase(phase: startingAngle, angle: $0.pos)) } + if mark.outer { + points = outerRing.arcPoints(lambdas: mappedNames) + } else { + points = innerRing.arcPoints(lambdas: mappedNames) + } + + for i in 0.. ([ChineseCalendar.NamedPosition], [CGColor]) { + var newPositions = [ChineseCalendar.NamedPosition]() + var newColors = [CGColor]() + for i in 0.. [DrawableText] { + let string: String + var hasSpace = false + let fontSize = font.font.pointSize + if compact { + hasSpace = tickName.firstIndex(of: " ") != nil + string = String(tickName.replacingOccurrences(of: " ", with: "")) + } else { + string = tickName + } + let attrStr = NSMutableAttributedString(string: string) + attrStr.addAttributes([.font: font.font, .foregroundColor: color], range: NSMakeRange(0, attrStr.length)) + + var boxTransform = CGAffineTransform(translationX: -point.position.x, y: -point.position.y) + let transform: CGAffineTransform + if point.direction <= CGFloat.pi/4 { + transform = CGAffineTransform(rotationAngle: -point.direction) + } else if point.direction < CGFloat.pi * 3/4 { + transform = CGAffineTransform(rotationAngle: CGFloat.pi/2 - point.direction) + } else if point.direction < CGFloat.pi * 5/4 { + transform = CGAffineTransform(rotationAngle: CGFloat.pi - point.direction) + } else if point.direction < CGFloat.pi * 7/4 { + transform = CGAffineTransform(rotationAngle: -point.direction - CGFloat.pi/2) + } else { + transform = CGAffineTransform(rotationAngle: -point.direction) + } + boxTransform = boxTransform.concatenating(transform) + boxTransform = boxTransform.concatenating(CGAffineTransform(translationX: point.position.x, y: point.position.y)) + + let characters = string.map { NSMutableAttributedString(string: String($0), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) } + let mean = CGFloat(characters.count - 1)/2 + + var text = [DrawableText]() + for i in 0.. CGFloat.pi/4 && point.direction < CGFloat.pi * 3/4) || (point.direction > CGFloat.pi * 5/4 && point.direction < CGFloat.pi * 7/4) { + box.origin.y += shift + box.origin.x += offset.width * pow(fontSize, 0.9) + } else { + box.origin.x -= shift + box.origin.y -= offset.height * pow(fontSize, 0.9) + } + let boxPath = CGPath(roundedRect: box, cornerWidth: cornerSize, cornerHeight: cornerSize, transform: &boxTransform) + text.append(DrawableText(string: AttributedString(characters[i]), position: box, boundingBox: boxPath, transform: boxTransform, color: color)) + } + return text +} + +fileprivate func prepareCoreText(text: String, offsetRatio: CGFloat, centerOffset: CGFloat, outerBound: RoundedRect, maxLength: Int, viewSize: CGSize, font: WatchFont) -> [DrawableText] { + let centerTextShortSize = min(outerBound._boundBox.width, outerBound._boundBox.height) * 0.31 + let centerTextLongSize = max(outerBound._boundBox.width, outerBound._boundBox.height) * 0.17 + let centerTextSize = min(centerTextShortSize, centerTextLongSize) * sqrt(5/CGFloat(maxLength)) + let isVertical = viewSize.height >= viewSize.width + let minSeparation = min(outerBound._boundBox.width, outerBound._boundBox.height) / 5 + var offset = min(centerTextSize * abs(offsetRatio) * sqrt(CGFloat(maxLength)/3), minSeparation) + offset *= offsetRatio >= 0 ? 1.0 : -1.0 + offset += centerTextSize * centerOffset * sqrt(CGFloat(maxLength)/3) + + var drawableTexts = [DrawableText]() + let centerFont = font.font.withSize(centerTextSize) + + let attrStr = NSMutableAttributedString(string: text) + attrStr.addAttributes([.font: centerFont, .foregroundColor: CGColor(gray: 1, alpha: 1)], range: NSMakeRange(0, attrStr.length)) + + var characters = attrStr.string.map { NSMutableAttributedString(string: String($0), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) } + if characters.count > maxLength { + characters = Array(characters[.. [CGFloat] { + return angles.map { angle in + if phase >= 0 { + return (angle + phase) % 1.0 + } else { + return (-angle + phase) % 1.0 + } + } +} diff --git a/Shared/Views/WatchFaceView.swift b/Shared/Views/WatchFaceView.swift new file mode 100644 index 0000000..2bcaaae --- /dev/null +++ b/Shared/Views/WatchFaceView.swift @@ -0,0 +1,351 @@ +// +// WatchFaceView.swift +// Chinese Time Watch +// +// Created by Leo Liu on 5/9/23. +// + +import SwiftUI + +struct ScaleEffectScale: EnvironmentKey { + static let defaultValue: CGFloat = 0 +} + +struct ScaleEffectAnchor: EnvironmentKey { + static let defaultValue = UnitPoint.center +} + +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 } + } +} + +private func calSubhourGradient(watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) -> WatchLayout.Gradient { + let startOfDay = chineseCalendar.startOfDay + let lengthOfDay = startOfDay.distance(to: chineseCalendar.startOfNextDay) + let fourthRingColor = WatchLayout.Gradient(locations: [0, 1], colors: [ + watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.startHour) / lengthOfDay) % 1.0), + watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.endHour) / lengthOfDay) % 1.0) + ], loop: false) + return fourthRingColor +} + +private enum Rings { + case date + case time +} + +private func ringMarks(for ring: Rings, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar, radius: CGFloat) -> ([Marks], [Marks]) { + switch ring { + case .date: + let eventInMonth = chineseCalendar.eventInMonth + let firstRingMarks = [Marks(outer: true, locations: chineseCalendar.planetPosition, colors: watchLayout.planetIndicator, radius: radius)] + let secondRingMarks = [ + Marks(outer: true, locations: eventInMonth.eclipse, colors: [watchLayout.eclipseIndicator], radius: radius), + Marks(outer: true, locations: eventInMonth.fullMoon, colors: [watchLayout.fullmoonIndicator], radius: radius), + Marks(outer: true, locations: eventInMonth.oddSolarTerm, colors: [watchLayout.oddStermIndicator], radius: radius), + Marks(outer: true, locations: eventInMonth.evenSolarTerm, colors: [watchLayout.evenStermIndicator], radius: radius) + ] + return (firstRingMarks, secondRingMarks) + + case .time: + let eventInDay = chineseCalendar.eventInDay + let sunMoonPositions = chineseCalendar.sunMoonPositions + let thirdRingMarks = [ + Marks(outer: true, locations: eventInDay.eclipse, colors: [watchLayout.eclipseIndicator], radius: radius), + Marks(outer: true, locations: eventInDay.fullMoon, colors: [watchLayout.fullmoonIndicator], radius: radius), + Marks(outer: true, locations: eventInDay.oddSolarTerm, colors: [watchLayout.oddStermIndicator], radius: radius), + Marks(outer: true, locations: eventInDay.evenSolarTerm, colors: [watchLayout.evenStermIndicator], radius: radius), + Marks(outer: false, locations: sunMoonPositions.solar, colors: watchLayout.sunPositionIndicator, radius: radius), + Marks(outer: false, locations: sunMoonPositions.lunar, colors: watchLayout.moonPositionIndicator, radius: radius) + ] + let eventInHour = chineseCalendar.eventInHour + let sunMoonSubhourPositions = chineseCalendar.sunMoonSubhourPositions + let fourthRingMarks = [ + Marks(outer: true, locations: eventInHour.eclipse, colors: [watchLayout.eclipseIndicator], radius: radius), + Marks(outer: true, locations: eventInHour.fullMoon, colors: [watchLayout.fullmoonIndicator], radius: radius), + Marks(outer: true, locations: eventInHour.oddSolarTerm, colors: [watchLayout.oddStermIndicator], radius: radius), + Marks(outer: true, locations: eventInHour.evenSolarTerm, colors: [watchLayout.evenStermIndicator], radius: radius), + Marks(outer: false, locations: sunMoonSubhourPositions.solar, colors: watchLayout.sunPositionIndicator, radius: radius), + Marks(outer: false, locations: sunMoonSubhourPositions.lunar, colors: watchLayout.moonPositionIndicator, radius: radius) + ] + return (thirdRingMarks, fourthRingMarks) + } +} + +func pressAnchor(pos: CGPoint?, size: CGSize, proxy: GeometryProxy) -> UnitPoint { + let center = CGPointMake(size.width / 2, size.height / 2) + let tapPosition: CGPoint + if var tapPos = pos { + tapPos.x -= (proxy.size.width - size.width) / 2 + tapPos.y -= (proxy.size.height - size.height) / 2 + tapPosition = tapPos + } else { + tapPosition = center + } + let maxEdge = max(size.width, size.height) + let direction = (tapPosition - center) / maxEdge + return UnitPoint(x: 0.5 + direction.x / 2, y: 0.5 + direction.y / 2) +} + +struct Watch: View { + static let frameOffset: CGFloat = 0.03 + + @Environment(\.colorScheme) var colorScheme + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground + @Environment(\.widgetRenderingMode) var widgetRenderingMode + @Environment(\.scaleEffectScale) var scaleEffectScale + @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + let shrink: Bool + let displayZeroRing: Bool + let displaySubquarter: Bool + let compact: Bool + let phase = StartingPhase() + let watchLayout: WatchLayout + let markSize: CGFloat + let widthScale: CGFloat + let chineseCalendar: ChineseCalendar + let centerOffset: CGFloat + let entityNotes: EntityNotes? + let shift: CGSize + + 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) { + self.shrink = shrink + self.displayZeroRing = displaySolarTerms + self.displaySubquarter = displaySubquarter + self.compact = compact + self.watchLayout = watchLayout + self.markSize = markSize + self.widthScale = widthScale + self.chineseCalendar = chineseCalendar + self.centerOffset = centerOffset + self.entityNotes = entityNotes + self.shift = if textShift { + CGSizeMake(watchLayout.horizontalTextOffset, watchLayout.verticalTextOffset) + } else { + CGSize.zero + } + } + + var body: some View { + + let watchLayout = switch widgetRenderingMode { + case .fullColor: + self.watchLayout + default: + self.watchLayout.monochrome + } + let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) + + let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor + let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor + let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor + let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor + let shadowDirection = chineseCalendar.currentHourInDay + + GeometryReader { proxy in + + let size = proxy.size + let shortEdge = min(size.width, size.height) + let cornerSize = watchLayout.cornerRadiusRatio * shortEdge + let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: (showsWidgetContainerBackground && shrink) ? Self.frameOffset * shortEdge : 0.0) + let firstRingOuter = displayZeroRing ? outerBound.shrink(by: ZeroRing.width * shortEdge * widthScale) : outerBound + let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let thirdRingOuter = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let fourthRingOuter = thirdRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let innerBound = fourthRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let (firstRingMarks, secondRingMarks) = ringMarks(for: .date, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * markSize) + let (thirdRingMarks, fourthRingMarks) = ringMarks(for: .time, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * markSize) + + ZStack { + if displayZeroRing { + let oddSTColor = colorScheme == .dark ? watchLayout.oddSolarTermTickColorDark : watchLayout.oddSolarTermTickColor + let evenSTColor = colorScheme == .dark ? watchLayout.evenSolarTermTickColorDark : watchLayout.evenSolarTermTickColor + 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, 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, 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, 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, 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) + 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, backColor: coreColor, 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) + } + } + } +} + +struct DateWatch: View { + static let frameOffset: CGFloat = 0.03 + + @Environment(\.scaleEffectScale) var scaleEffectScale + @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + @Environment(\.colorScheme) var colorScheme + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground + @Environment(\.widgetRenderingMode) var widgetRenderingMode + let shrink: Bool + let displayZeroRing: Bool + let compact: Bool + let phase = StartingPhase() + let watchLayout: WatchLayout + let markSize: CGFloat + let widthScale: CGFloat + let chineseCalendar: ChineseCalendar + let centerOffset: CGFloat + let entityNotes: EntityNotes? + + init(displaySolarTerms: Bool, compact: Bool, watchLayout: WatchLayout, markSize: CGFloat, chineseCalendar: ChineseCalendar, widthScale: CGFloat = 1, centerOffset: CGFloat = 0.05, entityNotes: EntityNotes? = nil, shrink: Bool = true) { + self.shrink = shrink + self.displayZeroRing = displaySolarTerms + self.compact = compact + self.watchLayout = watchLayout + self.markSize = markSize + self.widthScale = widthScale + self.chineseCalendar = chineseCalendar + self.centerOffset = centerOffset + self.entityNotes = entityNotes + } + + var body: some View { + + let watchLayout = switch widgetRenderingMode { + case .fullColor: + self.watchLayout + default: + self.watchLayout.monochrome + } + + let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor + let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor + let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor + let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor + let shadowDirection = chineseCalendar.currentHourInDay + + GeometryReader { proxy in + + let size = proxy.size + let shortEdge = min(size.width, size.height) + let cornerSize = watchLayout.cornerRadiusRatio * shortEdge + let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: (showsWidgetContainerBackground && shrink) ? Self.frameOffset * shortEdge : 0.0) + let firstRingOuter = displayZeroRing ? outerBound.shrink(by: ZeroRing.width * shortEdge * widthScale) : outerBound + let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + + let (firstRingMarks, secondRingMarks) = ringMarks(for: .date, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * markSize) + + ZStack { + if displayZeroRing { + let oddSTColor = colorScheme == .dark ? watchLayout.oddSolarTermTickColorDark : watchLayout.oddSolarTermTickColor + let evenSTColor = colorScheme == .dark ? watchLayout.evenSolarTermTickColorDark : watchLayout.evenSolarTermTickColor + 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, 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, 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) + + Core(viewSize: size, compact: compact, dateString: chineseCalendar.monthString, timeString: chineseCalendar.dayString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: coreColor, 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) + } + } + } +} + +struct TimeWatch: View { + static let frameOffset: CGFloat = 0.03 + + @Environment(\.scaleEffectScale) var scaleEffectScale + @Environment(\.scaleEffectAnchor) var scaleEffectAnchor + @Environment(\.colorScheme) var colorScheme + @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground + @Environment(\.widgetRenderingMode) var widgetRenderingMode + let shrink: Bool + let displayZeroRing: Bool + let displaySubquarter: Bool + let compact: Bool + let phase = StartingPhase() + let watchLayout: WatchLayout + let markSize: CGFloat + let widthScale: CGFloat + let chineseCalendar: ChineseCalendar + let centerOffset: CGFloat + let entityNotes: EntityNotes? + + 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) { + self.shrink = shrink + self.displayZeroRing = matchZeroRingGap + self.compact = compact + self.displaySubquarter = displaySubquarter + self.watchLayout = watchLayout + self.markSize = markSize + self.widthScale = widthScale + self.chineseCalendar = chineseCalendar + self.centerOffset = centerOffset + self.entityNotes = entityNotes + } + + var body: some View { + let watchLayout = switch widgetRenderingMode { + case .fullColor: + self.watchLayout + default: + self.watchLayout.monochrome + } + let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) + + let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor + let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor + let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor + let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor + let shadowDirection = chineseCalendar.currentHourInDay + + GeometryReader { proxy in + + let size = proxy.size + let shortEdge = min(size.width, size.height) + let cornerSize = watchLayout.cornerRadiusRatio * shortEdge + let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: (showsWidgetContainerBackground && shrink) ? Self.frameOffset * shortEdge : 0.0) + let firstRingOuter = displayZeroRing ? outerBound.shrink(by: ZeroRing.width * shortEdge * widthScale) : outerBound + let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let (thirdRingMarks, fourthRingMarks) = ringMarks(for: .time, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * markSize) + + 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, 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, 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) + + 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, backColor: coreColor, 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) + } + } + } +} diff --git a/Shared/WatchConnectivity.swift b/Shared/WatchConnectivity.swift index 6f08adb..4d9ca4e 100644 --- a/Shared/WatchConnectivity.swift +++ b/Shared/WatchConnectivity.swift @@ -21,14 +21,18 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { if let newLayout = message["layout"] as? String { #if os(watchOS) - DispatchQueue.main.async { - WatchLayout.shared.update(from: newLayout) - DataContainer.shared.saveLayout(WatchLayout.shared.encode()) + Task(priority: .userInitiated) { + let modelContext = ThemeData.context + let watchLayout = WatchLayout.shared + watchLayout.update(from: newLayout) + watchLayout.saveDefault(context: modelContext) + try? modelContext.save() + LocationManager.shared.enabled = watchLayout.locationEnabled } #endif } else if let request = message["request"] as? String, request == "layout" { #if os(iOS) - _ = self.sendLayout(WatchLayout.shared.encode(includeOffset: false)) + self.sendLayout(WatchLayout.shared.encode(includeOffset: false)) #endif } } @@ -44,26 +48,23 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { } #endif - func sendLayout(_ message: String) -> Bool { - guard WCSession.default.activationState == .activated else { return false } + func sendLayout(_ message: String) { + guard WCSession.default.activationState == .activated else { return } #if os(iOS) - guard WCSession.default.isWatchAppInstalled else { return false } + guard WCSession.default.isWatchAppInstalled else { return } #else - guard WCSession.default.isCompanionAppInstalled else { return false } + guard WCSession.default.isCompanionAppInstalled else { return } #endif - let backgroundQueue = DispatchQueue(label: "background_queue", qos: .background) - backgroundQueue.async { + Task(priority: .background) { WCSession.default.sendMessage(["layout": message], replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } } - return true } #if os(watchOS) func requestLayout() { - let backgroundQueue = DispatchQueue(label: "background_queue", qos: .background) - backgroundQueue.async { + Task(priority: .background) { WCSession.default.sendMessage(["request": "layout"], replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } diff --git a/Watch/Assets.xcassets/AccentColor.colorset/Contents.json b/Watch/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..e12e779 100644 --- a/Watch/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Watch/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,10 @@ { "colors" : [ { + "color" : { + "platform" : "universal", + "reference" : "systemPinkColor" + }, "idiom" : "universal" } ], diff --git a/Watch/Assets.xcassets/AppIcon.appiconset/watch.png b/Watch/Assets.xcassets/AppIcon.appiconset/watch.png index d613e65..e76a2d8 100644 Binary files a/Watch/Assets.xcassets/AppIcon.appiconset/watch.png and b/Watch/Assets.xcassets/AppIcon.appiconset/watch.png differ diff --git a/Watch/Assets.xcassets/Contents.json b/Watch/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/Watch/Assets.xcassets/Contents.json +++ b/Watch/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/Watch/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents b/Watch/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents deleted file mode 100644 index 211f48c..0000000 --- a/Watch/ChineseTime.xcdatamodeld/ChineseTime.xcdatamodel/contents +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Watch/ChineseTimeApp.swift b/Watch/ChineseTimeApp.swift deleted file mode 100644 index e6459f7..0000000 --- a/Watch/ChineseTimeApp.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ChineseTimeApp.swift -// ChineseTime Watch App -// -// Created by Leo Liu on 5/3/23. -// - -import SwiftUI - -@main -struct ChineseTime_Watch_App: App { - init() { - DataContainer.shared.loadSave() - let _ = WatchConnectivityManager.shared - LocationManager.shared.manager.requestWhenInUseAuthorization() - } - - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Watch/Chinese Time Watch.entitlements b/Watch/ChineseTimeWatch.entitlements similarity index 100% rename from Watch/Chinese Time Watch.entitlements rename to Watch/ChineseTimeWatch.entitlements diff --git a/Watch/ContentView.swift b/Watch/ContentView.swift deleted file mode 100644 index e93e46b..0000000 --- a/Watch/ContentView.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// ContentView.swift -// ChineseTime Watch App -// -// Created by Leo Liu on 5/3/23. -// - -import SwiftUI -import WidgetKit - -struct ContentView: View { - @Environment(\.scenePhase) var scenePhase - @StateObject var locationManager = LocationManager.shared - @StateObject var watchLayout = WatchLayout.shared - - let timer = Timer.publish(every: Watch.updateInterval, on: .main, in: .common).autoconnect() - @State var cornerRadius: CGFloat = 0 - @State var adjustTime: Date? - @State var adjustTimeTarget: Bool = true - @State var size: CGSize = .zero - @State var refresh = false - @State var displayTime: Date? - @State var dual: Bool = false - - var body: some View { - return GeometryReader { proxy in - NavigationStack { - ScrollView { - VStack(spacing: 20) { - if dual { - DualWatch(compact: true, refresh: refresh, - watchLayout: watchLayout, displayTime: displayTime, timezone: Calendar.current.timeZone, realLocation: locationManager.location) - .frame(width: size.width, height: size.height) - .navigationTitle(NSLocalizedString("華曆", comment: "App Name")) - .navigationBarTitleDisplayMode(.inline) - .onReceive(timer) { _ in - refresh.toggle() - } - .onChange(of: scenePhase) { newPhase in - if newPhase == .active { - WatchConnectivityManager.shared.requestLayout() - WidgetCenter.shared.reloadAllTimelines() - } - } - .onAppear { - locationManager.requestLocation(completion: nil) - } - } else { - Watch(compact: true, refresh: refresh, - watchLayout: watchLayout, displayTime: displayTime, timezone: Calendar.current.timeZone, realLocation: locationManager.location) - .frame(width: size.width, height: size.height) - .navigationTitle(NSLocalizedString("華曆", comment: "App Name")) - .navigationBarTitleDisplayMode(.inline) - .onReceive(timer) { _ in - refresh.toggle() - } - .onChange(of: scenePhase) { newPhase in - if newPhase == .active { - WatchConnectivityManager.shared.requestLayout() - WidgetCenter.shared.reloadAllTimelines() - } - } - .onAppear { - locationManager.requestLocation(completion: nil) - } - } - Spacer(minLength: 10) - VStack(spacing: 0) { - Text(NSLocalizedString("圓角比例", comment: "Corner radius ratio")) - .font(.body) - .frame(maxWidth: .infinity, alignment: .leading) - HStack { - Button(action: { - cornerRadius = max(0.3, cornerRadius - 0.1) - watchLayout.cornerRadiusRatio = cornerRadius - refresh.toggle() - DataContainer.shared.saveLayout(watchLayout.encode()) - }) { - Image(systemName: "minus") - .font(Font.system(.title3, design: .rounded, weight: .black)) - .padding() - .foregroundColor(.black) - .background { - Circle() - .fill(Color.pink) - .frame(width: 35, height: 35) - } - } - .padding(.all, 0) - .buttonStyle(.borderless) - Text(String(format: "%.1f", cornerRadius)) - .onAppear { - cornerRadius = watchLayout.cornerRadiusRatio - } - .font(Font.system(.title, design: .rounded, weight: .black)) - .padding() - .foregroundColor(.white) - .frame(maxWidth: .infinity) - Button(action: { - cornerRadius = min(0.9, cornerRadius + 0.1) - watchLayout.cornerRadiusRatio = cornerRadius - refresh.toggle() - DataContainer.shared.saveLayout(watchLayout.encode()) - }) { - Image(systemName: "plus") - .font(Font.system(.title3, design: .rounded, weight: .black)) - .padding() - .foregroundColor(.black) - .background { - Circle() - .fill(Color.pink) - .frame(width: 35, height: 35) - } - } - .padding(.all, 0) - .buttonStyle(.borderless) - } - } - NavigationLink(NSLocalizedString("調時", comment: "Change Time")) { - ScrollView { - VStack(spacing: 10) { - Text(adjustTime?.formatted(date: .numeric, time: .omitted) ?? "") - .font(Font.system(.title3, design: .rounded, weight: .bold)) - .frame(maxWidth: .infinity, minHeight: 25) - .lineLimit(1) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .stroke(Color.green, lineWidth: adjustTimeTarget ? 0 : 1) - .padding(.all, 1) - ) - .onTapGesture { - adjustTimeTarget = false - } - Text(adjustTime?.formatted(date: .omitted, time: .shortened) ?? "") - .font(Font.system(.title3, design: .rounded, weight: .bold)) - .frame(maxWidth: .infinity, minHeight: 25) - .lineLimit(1) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .stroke(Color.green, lineWidth: adjustTimeTarget ? 1 : 0) - .padding(.all, 1) - ) - .onTapGesture { - adjustTimeTarget = true - } - HStack { - Button(action: { - adjustTime = adjustTime?.advanced(by: adjustTimeTarget ? -3600 : -3600 * 24) - displayTime = adjustTime - }) { - Image(systemName: "minus") - .font(Font.system(.title3, design: .rounded, weight: .black)) - .foregroundColor(.black) - } - .buttonStyle(.borderedProminent) - .tint(Color.pink) - .buttonBorderShape(.capsule) - Button(action: { - adjustTime = adjustTime?.advanced(by: adjustTimeTarget ? 3600 : 3600 * 24) - displayTime = adjustTime - }) { - Image(systemName: "plus") - .font(Font.system(.title3, design: .rounded, weight: .black)) - .foregroundColor(.black) - } - .buttonStyle(.borderedProminent) - .tint(Color.pink) - .buttonBorderShape(.capsule) - } - Button(action: { - adjustTime = Date() - displayTime = nil - }) { - Image(systemName: "arrow.clockwise") - .font(Font.system(.title3, design: .rounded, weight: .black)) - .foregroundColor(.black) - } - .buttonStyle(.borderedProminent) - .tint(Color.pink) - .buttonBorderShape(.capsule) - } - .navigationTitle(NSLocalizedString("調時", comment: "Change Time")) - .onAppear { - adjustTime = displayTime ?? Date() - } - .onDisappear { - adjustTime = nil - } - } - } - .buttonStyle(.borderedProminent) - .tint(Color.pink) - Toggle(NSLocalizedString("分列日時", comment: "Split Date and Time"), isOn: $dual) - .onChange(of: dual) { newValue in - if let userDefaults = UserDefaults(suiteName: DataContainer.groupId) { - userDefaults.set(newValue, forKey: "ChinsesTime.DualWatchDisplay") - userDefaults.synchronize() - } - } - .toggleStyle(.button) - .tint(.pink) - Text(NSLocalizedString("更多設置請移步 iOS App,可於手機與手錶間自動同步", comment: "Hint for syncing between watch and phone")) - .frame(maxWidth: .infinity) - .font(Font.footnote) - .foregroundColor(Color.secondary) - } - } - } - .onAppear { - dual = UserDefaults(suiteName: DataContainer.groupId)?.bool(forKey: "ChinsesTime.DualWatchDisplay") ?? dual - size = proxy.size - } - } - .ignoresSafeArea(edges: [.bottom, .horizontal]) - } -} - -struct ContentView_Previews: PreviewProvider { - init() { - DataContainer.shared.loadSave() - } - - static var previews: some View { - DualWatch(compact: true, refresh: false, watchLayout: WatchLayout.shared) - .previewDevice(PreviewDevice(rawValue: "Apple Watch Series 8 (41mm)")) - .previewDisplayName("41mm") - DualWatch(compact: true, refresh: false, watchLayout: WatchLayout.shared) - .previewDevice(PreviewDevice(rawValue: "Apple Watch Series 8 (45mm)")) - .previewDisplayName("45mm") - Watch(compact: true, refresh: false, watchLayout: WatchLayout.shared) - .previewDevice(PreviewDevice(rawValue: "Apple Watch Ultra (49mm)")) - .previewDisplayName("49mm") - } -} diff --git a/Watch/Chinese-Time-Watch-Info.plist b/Watch/Info.plist similarity index 88% rename from Watch/Chinese-Time-Watch-Info.plist rename to Watch/Info.plist index 622f178..5c5e95c 100644 --- a/Watch/Chinese-Time-Watch-Info.plist +++ b/Watch/Info.plist @@ -4,10 +4,13 @@ ITSAppUsesNonExemptEncryption + LSHasLocalizedDisplayName + NSUserActivityTypes CircularIntent CurveIntent + RectIntent SingleLineIntent UIAppFonts diff --git a/Watch/Layout.swift b/Watch/Layout.swift new file mode 100644 index 0000000..365da2e --- /dev/null +++ b/Watch/Layout.swift @@ -0,0 +1,53 @@ +// +// WatchLayout.swift +// Chinese Time +// +// Created by Leo Liu on 5/11/23. +// + +import SwiftUI +import Observation + +@Observable final class WatchLayout: MetaWatchLayout { + static var shared = WatchLayout() + + var textFont = UIFont.systemFont(ofSize: 14, weight: .regular) + var centerFont = UIFont(name: "SourceHanSansKR-Heavy", size: 14)! + var dualWatch = false + + private override init() { + super.init() + } + + override func encode(includeOffset: Bool = true, includeColor: Bool = true, includeConfig: Bool = true) -> String { + var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor, includeConfig: includeConfig) + encoded += "dualWatch: \(dualWatch)\n" + return encoded + } + + override func update(from values: [String: String]) { + super.update(from: values) + if let dual = values["dualWatch"]?.boolValue { + dualWatch = dual + } + } + + var monochrome: Self { + let emptyLayout = Self.init() + emptyLayout.update(from: self.encode(includeColor: false)) + return emptyLayout + } + + func binding(_ keyPath: ReferenceWritableKeyPath) -> Binding { + return Binding(get: { self[keyPath: keyPath] }, set: { self[keyPath: keyPath] = $0 }) + } +} + +@Observable final class WatchSetting { + static let shared = WatchSetting() + + var size: CGSize = .zero + var displayTime: Date? = nil + + private init() {} +} diff --git a/Watch/Views/ContentView.swift b/Watch/Views/ContentView.swift new file mode 100644 index 0000000..5d934e7 --- /dev/null +++ b/Watch/Views/ContentView.swift @@ -0,0 +1,74 @@ +// +// ContentView.swift +// ChineseTime Watch App +// +// Created by Leo Liu on 5/3/23. +// + +import SwiftUI +import WidgetKit + +struct WatchFaceTab: View { + @Environment(\.watchSetting) var watchSetting + let proxy: GeometryProxy + let tab: Tab + + init(proxy: GeometryProxy, @ViewBuilder content: () -> Tab) { + self.proxy = proxy + self.tab = content() + } + + var body: some View { + TabView { + tab + NavigationStack { + Setting() + } + } + .tabViewStyle(VerticalPageTabViewStyle(transitionStyle: .identity)) + .onAppear { + watchSetting.size = proxy.size + } + } +} + +struct ContentView: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.scenePhase) var scenePhase + @Environment(\.modelContext) private var modelContext + + var body: some View { + GeometryReader { proxy in + if watchLayout.dualWatch { + WatchFaceTab(proxy: proxy) { + WatchFaceDate() + .ignoresSafeArea() + WatchFaceTime() + .ignoresSafeArea() + } + } else { + WatchFaceTab(proxy: proxy) { + WatchFaceFull() + .ignoresSafeArea() + } + } + } + .ignoresSafeArea() + .onChange(of: scenePhase) { _, newPhase in + switch newPhase { + case .active: + WatchConnectivityManager.shared.requestLayout() + case .inactive, .background: + WidgetCenter.shared.reloadAllTimelines() + @unknown default: + break + } + } + } +} + +#Preview("Watch Face") { + ContentView() + .environment(\.chineseCalendar, .init(time: .now, compact: true)) + .modelContainer(ThemeData.container) +} diff --git a/Watch/Views/DateTimeAdjust.swift b/Watch/Views/DateTimeAdjust.swift new file mode 100644 index 0000000..5e8ea97 --- /dev/null +++ b/Watch/Views/DateTimeAdjust.swift @@ -0,0 +1,86 @@ +// +// SwiftUIView.swift +// Chinese Time Watch +// +// Created by Leo Liu on 6/26/23. +// + +import SwiftUI +import Observation + +@Observable fileprivate class TimeManager { + var chineseCalendar: ChineseCalendar? + var watchSetting: WatchSetting? + + var time: Date { + get { + watchSetting?.displayTime ?? chineseCalendar?.time ?? .now + } set { + watchSetting?.displayTime = newValue + chineseCalendar?.update(time: watchSetting?.displayTime ?? .now) + } + } + + var isCurrent: Bool { + get { + watchSetting?.displayTime == nil + } set { + if newValue { + watchSetting?.displayTime = nil + } else { + watchSetting?.displayTime = chineseCalendar?.time + } + chineseCalendar?.update(time: watchSetting?.displayTime ?? .now) + } + } + + func setup(watchSetting: WatchSetting, chineseCalendar: ChineseCalendar) { + self.watchSetting = watchSetting + self.chineseCalendar = chineseCalendar + } +} + +struct DateTimeAdjust: View { + @Environment(\.watchSetting) var watchSetting + @Environment(\.chineseCalendar) var chineseCalendar + @State private var timeManager = TimeManager() + + var body: some View { + VStack(spacing: 10) { + DatePicker(selection: $timeManager.time, in: ChineseCalendar.start...ChineseCalendar.end, displayedComponents: [.date]) { + Text("日", comment: "Date") + } + .minimumScaleFactor(0.75) + DatePicker(selection: $timeManager.time, displayedComponents: [.hourAndMinute]) { + Text("時", comment: "Time") + } + .minimumScaleFactor(0.75) + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(action: { + timeManager.isCurrent = true + }) { + Label { + Text("今", comment: "Now") + } icon: { + Image(systemName: "clock.arrow.circlepath") + } + } + .disabled(timeManager.isCurrent) + } + } + .navigationTitle(Text("擇時", comment: "Choose Data & Time")) + .task { + timeManager.setup(watchSetting: watchSetting, chineseCalendar: chineseCalendar) + } + .onDisappear { + chineseCalendar.update(time: watchSetting.displayTime ?? Date.now) + } + } +} + + +#Preview("Datetime Adjust") { + DateTimeAdjust() +} diff --git a/Watch/Views/Setting.swift b/Watch/Views/Setting.swift new file mode 100644 index 0000000..cee8b9c --- /dev/null +++ b/Watch/Views/Setting.swift @@ -0,0 +1,63 @@ +// +// Setting.swift +// Chinese Time Watch +// +// Created by Leo Liu on 6/26/23. +// + +import SwiftUI + +struct Setting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var settings + @Environment(\.modelContext) var modelContext + let range: ClosedRange = 0.3...0.9 + let step: CGFloat = 0.1 + var dualWatch: Binding { + .init(get: { watchLayout.dualWatch }, set: { newValue in + watchLayout.dualWatch = newValue + watchLayout.saveDefault(context: modelContext) + }) + } + var cornerRadius: Binding { + .init(get: { watchLayout.cornerRadiusRatio }, set: { newValue in + watchLayout.cornerRadiusRatio = newValue + watchLayout.saveDefault(context: modelContext) + }) + } + + var body: some View { + List { + Section { + Stepper(value: cornerRadius, in: range, step: step) { + Text(String(format: "%.1f", cornerRadius.wrappedValue)) + .font(.title.bold()) + .fontDesign(.rounded) + } + .focusable(false) + } header: { + Text("圓角比例", comment: "Corner radius ratio") + } + + Section { + NavigationLink(NSLocalizedString("調時", comment: "Change Time")) { + 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") + } + } + .navigationTitle(Text("設置", comment: "Settings View")) + .navigationBarTitleDisplayMode(.large) + } +} + +#Preview("Setting") { + NavigationStack { + Setting() + .modelContainer(ThemeData.container) + } +} diff --git a/Watch/Views/WatchFace.swift b/Watch/Views/WatchFace.swift new file mode 100644 index 0000000..53e2a68 --- /dev/null +++ b/Watch/Views/WatchFace.swift @@ -0,0 +1,138 @@ +// +// WatchFace.swift +// Chinese Time Watch +// +// Created by Leo Liu on 6/30/23. +// + +import SwiftUI + +@MainActor +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 + + 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 + } + } + + var body: some View { + GeometryReader { proxy in + ZStack { + content() + .environment(\.scaleEffectScale, longPressed ? -0.1 : 0.0) + .environment(\.scaleEffectAnchor, pressAnchor(pos: tapPos, size: proxy.size, proxy: proxy)) + .gesture(longPress) + .simultaneousGesture(tapGesture) + Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) + } + .animation(.easeInOut(duration: 0.2), value: tapPos) + } + } +} + +@MainActor +struct WatchFaceDate: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + @Environment(\.chineseCalendar) var chineseCalendar + @State var entityPresenting = EntitySelection() + + 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) + .frame(width: watchSetting.size.width, height: watchSetting.size.height) + } + } +} + +@MainActor +struct WatchFaceTime: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + @Environment(\.chineseCalendar) var chineseCalendar + @State var entityPresenting = EntitySelection() + + 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) + .frame(width: watchSetting.size.width, height: watchSetting.size.height) + } + } +} + +@MainActor +struct WatchFaceFull: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + @Environment(\.chineseCalendar) var chineseCalendar + @State var entityPresenting = EntitySelection() + + 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) + .frame(width: watchSetting.size.width, height: watchSetting.size.height) + } + } +} + +#Preview("Date") { + @Environment(\.watchSetting) var watchSetting + + return GeometryReader { proxy in + WatchFaceDate() + .onAppear{ + watchSetting.size = proxy.size + } + } + .ignoresSafeArea() +} + +#Preview("Time") { + @Environment(\.watchSetting) var watchSetting + + return GeometryReader { proxy in + WatchFaceTime() + .onAppear{ + watchSetting.size = proxy.size + } + } + .ignoresSafeArea() +} + +#Preview("Full") { + @Environment(\.watchSetting) var watchSetting + + return GeometryReader { proxy in + WatchFaceFull() + .onAppear{ + watchSetting.size = proxy.size + } + } + .ignoresSafeArea() +} diff --git a/Watch/WatchFaceBasics.swift b/Watch/WatchFaceBasics.swift deleted file mode 100644 index b90eede..0000000 --- a/Watch/WatchFaceBasics.swift +++ /dev/null @@ -1,400 +0,0 @@ -// -// WatchFace.swift -// ChineseTime Watch App -// -// Created by Leo Liu on 5/4/23. -// - -import SwiftUI - -struct WatchFont { - #if os(macOS) - var font: NSFont - init(_ font: NSFont) { - self.font = font - } - #else - var font: UIFont - init(_ font: UIFont) { - self.font = font - } - #endif -} - -struct ZeroRing: View { - static let width: CGFloat = 0.05 - let width: CGFloat - let viewSize: CGSize - let compact: Bool - let textFont: WatchFont - let outerRing: RoundedRect - let startingAngle: CGFloat - let oddTicks: [CGFloat] - let evenTicks: [CGFloat] - let oddColor: CGColor - let evenColor: CGColor - let oddTexts: [String] - let evenTexts: [String] - - var body: some View { - let shortEdge = min(self.viewSize.width, self.viewSize.height) - let longEdge = max(self.viewSize.width, self.viewSize.height) - let fontSize: CGFloat = min(shortEdge * 0.03, longEdge * 0.025) * (compact ? 1.5 : 1.0) - let majorLineWidth = shortEdge/300 - - Canvas { context, _ in - - let textRing = outerRing.shrink(by: (width + 0.003)/2 * shortEdge) - let innerBound = outerRing.shrink(by: width * shortEdge) - let ringBoundPath = outerRing.path - ringBoundPath.addPath(innerBound.path) - - context.clip(to: Path(ringBoundPath), style: FillStyle(eoFill: true)) - let oddTicksPath = outerRing.arcPosition(lambdas: changePhase(phase: startingAngle, angles: oddTicks), width: 0.1 * shortEdge) - context.stroke(Path(oddTicksPath), with: .color(Color(cgColor: oddColor)), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - let evenTicksPath = outerRing.arcPosition(lambdas: changePhase(phase: startingAngle, angles: evenTicks), width: 0.1 * shortEdge) - context.stroke(Path(evenTicksPath), with: .color(Color(cgColor: evenColor)), style: StrokeStyle(lineWidth: majorLineWidth, lineCap: .square, lineJoin: .bevel, miterLimit: .leastNonzeroMagnitude)) - - let font = textFont.font.withSize(fontSize) - var drawableTexts = [DrawableText]() - - let oddPoints = textRing.arcPoints(lambdas: changePhase(phase: startingAngle, angles: oddTicks)) - for i in 0..= 0 && $0 < 1 })) - } else { - points = innerRing.arcPoints(lambdas: changePhase(phase: startingAngle, angles: mark.locations.filter { $0 >= 0 && $0 < 1 })) - } - var markContext = context - markContext.addFilter(.shadow(color: Color(white: 0, opacity: 0.5), radius: mark.radius/2, x: 0, y: 0)) - for i in 0.. [DrawableText] { - let centerTextShortSize = min(outerBound._boundBox.width, outerBound._boundBox.height) * 0.31 - let centerTextLongSize = max(outerBound._boundBox.width, outerBound._boundBox.height) * 0.17 - let centerTextSize = min(centerTextShortSize, centerTextLongSize) * (compact ? 1.1 : 1.0) * sqrt(5/CGFloat(maxLength)) - let isVertical = viewSize.height >= viewSize.width - let offset = centerTextSize * offsetRatio * sqrt(CGFloat(maxLength)/3) - - var drawableTexts = [DrawableText]() - let centerFont = font.font.withSize(centerTextSize) - - let attrStr = NSMutableAttributedString(string: text) - attrStr.addAttributes([.font: centerFont, .foregroundColor: CGColor(gray: 1, alpha: 1)], range: NSMakeRange(0, attrStr.length)) - - var characters = attrStr.string.map { NSMutableAttributedString(string: String($0), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) } - if characters.count > maxLength { - characters = Array(characters[.. ([CGFloat], [CGColor]) { - var newPositions = [CGFloat]() - var newColors = [CGColor]() - for i in 0.. [DrawableText] { - let string: String - var hasSpace = false - let fontSize = font.font.pointSize - if compact { - hasSpace = tickName.firstIndex(of: " ") != nil - string = String(tickName.replacingOccurrences(of: " ", with: "")) - } else { - string = tickName - } - let attrStr = NSMutableAttributedString(string: string) - attrStr.addAttributes([.font: font.font, .foregroundColor: color], range: NSMakeRange(0, attrStr.length)) - - var boxTransform = CGAffineTransform(translationX: -point.position.x, y: -point.position.y) - let transform: CGAffineTransform - if point.direction <= CGFloat.pi/4 { - transform = CGAffineTransform(rotationAngle: -point.direction) - } else if point.direction < CGFloat.pi * 3/4 { - transform = CGAffineTransform(rotationAngle: CGFloat.pi/2 - point.direction) - } else if point.direction < CGFloat.pi * 5/4 { - transform = CGAffineTransform(rotationAngle: CGFloat.pi - point.direction) - } else if point.direction < CGFloat.pi * 7/4 { - transform = CGAffineTransform(rotationAngle: -point.direction - CGFloat.pi/2) - } else { - transform = CGAffineTransform(rotationAngle: -point.direction) - } - boxTransform = boxTransform.concatenating(transform) - boxTransform = boxTransform.concatenating(CGAffineTransform(translationX: point.position.x, y: point.position.y)) - - let characters = string.map { NSMutableAttributedString(string: String($0), attributes: attrStr.attributes(at: 0, effectiveRange: nil)) } - let mean = CGFloat(characters.count - 1)/2 - - var text = [DrawableText]() - for i in 0.. CGFloat.pi/4 && point.direction < CGFloat.pi * 3/4) || (point.direction > CGFloat.pi * 5/4 && point.direction < CGFloat.pi * 7/4) { - box.origin.y += shift - } else { - box.origin.x -= shift - } - let boxPath = CGPath(roundedRect: box, cornerWidth: cornerSize, cornerHeight: cornerSize, transform: &boxTransform) - text.append(DrawableText(string: AttributedString(characters[i]), position: box, boundingBox: boxPath, transform: boxTransform, color: color)) - } - return text -} - -private func changePhase(phase: CGFloat, angles: [CGFloat]) -> [CGFloat] { - return angles.map { angle in - if phase >= 0 { - return (angle + phase) % 1.0 - } else { - return (-angle + phase) % 1.0 - } - } -} diff --git a/Watch/WatchFaceView.swift b/Watch/WatchFaceView.swift deleted file mode 100644 index c9ae0c7..0000000 --- a/Watch/WatchFaceView.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// WatchFaceView.swift -// Chinese Time Watch -// -// Created by Leo Liu on 5/9/23. -// - -import SwiftUI - -private func calSubhourGradient(watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) -> WatchLayout.Gradient { - let startOfDay = chineseCalendar.startOfDay - let lengthOfDay = startOfDay.distance(to: chineseCalendar.startOfNextDay) - let fourthRingColor = WatchLayout.Gradient(locations: [0, 1], colors: [ - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.startHour) / lengthOfDay) % 1.0), - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.endHour) / lengthOfDay) % 1.0) - ], loop: false) - return fourthRingColor -} - -private func allRingMarks(watchLayout: WatchLayout, chineseCalendar: ChineseCalendar, radius: CGFloat) -> ([Marks], [Marks], [Marks], [Marks]) { - let eventInMonth = chineseCalendar.eventInMonth - let firstRingMarks = [Marks(outer: true, locations: chineseCalendar.planetPosition.map { $0.pos }, colors: watchLayout.planetIndicator, radius: radius)] - let secondRingMarks = [ - Marks(outer: true, locations: eventInMonth.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius) - ] - let eventInDay = chineseCalendar.eventInDay - let sunMoonPositions = chineseCalendar.sunMoonPositions - let thirdRingMarks = [ - Marks(outer: true, locations: eventInDay.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius), - Marks(outer: false, locations: sunMoonPositions.solar.map { option in option.map { value in value.pos } }, colors: watchLayout.sunPositionIndicator, radius: radius), - Marks(outer: false, locations: sunMoonPositions.lunar.map { option in option.map { value in value.pos } }, colors: watchLayout.moonPositionIndicator, radius: radius) - ] - let eventInHour = chineseCalendar.eventInHour - let sunMoonSubhourPositions = chineseCalendar.sunMoonSubhourPositions - let fourthRingMarks = [ - Marks(outer: true, locations: eventInHour.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius), - Marks(outer: false, locations: sunMoonSubhourPositions.solar.map { option in option.map { value in value.pos } }, colors: watchLayout.sunPositionIndicator, radius: radius), - Marks(outer: false, locations: sunMoonSubhourPositions.lunar.map { option in option.map { value in value.pos } }, colors: watchLayout.moonPositionIndicator, radius: radius) - ] - return (first: firstRingMarks, second: secondRingMarks, third: thirdRingMarks, fourth: fourthRingMarks) -} - -struct Watch: View { - private static let majorUpdateInterval: CGFloat = 3600 - private static let minorUpdateInterval: CGFloat = majorUpdateInterval / 12 - static let updateInterval: CGFloat = 14.4 - static let frameOffset: CGFloat = 0 - - @State var size = CGSize.zero - @State var refresh: Bool = false - let compact: Bool - let phase = StartingPhase() - let watchLayout: WatchLayout - let chineseCalendar: ChineseCalendar - var displayTime: Date? = nil - var timezone = Calendar.current.timeZone - var realLocation: CGPoint? = nil - - var location: CGPoint? { - realLocation ?? watchLayout.location - } - - init(compact: Bool, refresh: Bool, watchLayout: WatchLayout, displayTime: Date? = nil, timezone: TimeZone? = nil, realLocation: CGPoint? = nil) { - self.compact = compact - self.watchLayout = watchLayout - self.displayTime = displayTime - self.timezone = timezone ?? Calendar.current.timeZone - self.realLocation = realLocation - self.chineseCalendar = ChineseCalendar(time: displayTime ?? Date(), timezone: self.timezone, location: realLocation ?? watchLayout.location, compact: compact) - self.refresh = refresh - } - - var body: some View { - let shortEdge = min(self.size.width, self.size.height) - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: Watch.frameOffset * shortEdge) - let firstRingOuter = outerBound.shrink(by: ZeroRing.width * shortEdge) - let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge) - let thirdRingOuter = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge) - let fourthRingOuter = thirdRingOuter.shrink(by: Ring.paddedWidth * shortEdge) - let innerBound = fourthRingOuter.shrink(by: Ring.paddedWidth * shortEdge) - let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) - - let _ = chineseCalendar.update(time: displayTime ?? Date(), timezone: timezone, location: location) - - let (firstRingMarks, secondRingMarks, thirdRingMarks, fourthRingMarks) = allRingMarks(watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 1.5) - - let shadowDirection = chineseCalendar.currentHourInDay - - GeometryReader { proxy in - ZStack { - ZeroRing(width: ZeroRing.width, 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: watchLayout.oddSolarTermTickColorDark, evenColor: watchLayout.evenSolarTermTickColorDark, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese) - Ring(width: Ring.paddedWidth, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection) - Core(viewSize: size, compact: compact, dateString: chineseCalendar.dateString, timeString: chineseCalendar.timeString, font: WatchFont(watchLayout.centerFont), maxLength: 5, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: watchLayout.innerColorDark, centerOffset: 0.1, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - } - .ignoresSafeArea(edges: .bottom) - } -} - -struct DualWatch: View { - private static let majorUpdateInterval: CGFloat = 3600 - private static let minorUpdateInterval: CGFloat = majorUpdateInterval / 12 - static let updateInterval: CGFloat = 14.4 - static let frameOffset: CGFloat = 0 - static var timer: Timer? - - @State var size = CGSize.zero - @State var refresh: Bool = false - @State private var hideDots: Bool = true - @State private var selectedPageIndex = 0 - let compact: Bool - let phase = StartingPhase() - let watchLayout: WatchLayout - let chineseCalendar: ChineseCalendar - var displayTime: Date? = nil - var timezone = Calendar.current.timeZone - var realLocation: CGPoint? = nil - - var location: CGPoint? { - realLocation ?? watchLayout.location - } - - init(compact: Bool, refresh: Bool, watchLayout: WatchLayout, displayTime: Date? = nil, timezone: TimeZone? = nil, realLocation: CGPoint? = nil) { - self.compact = compact - self.watchLayout = watchLayout - self.displayTime = displayTime - self.timezone = timezone ?? Calendar.current.timeZone - self.realLocation = realLocation - self.chineseCalendar = ChineseCalendar(time: displayTime ?? Date(), timezone: self.timezone, location: realLocation ?? watchLayout.location, compact: compact) - self.refresh = refresh - } - - var body: some View { - let shortEdge = min(self.size.width, self.size.height) - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: Watch.frameOffset * shortEdge) - let firstRingOuter = outerBound.shrink(by: ZeroRing.width * shortEdge * 1.2) - let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) - - let _ = chineseCalendar.update(time: displayTime ?? Date(), timezone: timezone, location: location) - - let (firstRingMarks, secondRingMarks, thirdRingMarks, fourthRingMarks) = allRingMarks(watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 1.5) - - let shadowDirection = chineseCalendar.currentHourInDay - - GeometryReader { proxy in - TabView(selection: $selectedPageIndex) { - ZStack { - ZeroRing(width: ZeroRing.width * 1.2, 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: watchLayout.oddSolarTermTickColorDark, evenColor: watchLayout.evenSolarTermTickColorDark, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese) - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection) - - Core(viewSize: size, compact: compact, dateString: chineseCalendar.monthString, timeString: chineseCalendar.dayString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: watchLayout.innerColorDark, centerOffset: 0.05, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - .ignoresSafeArea() - .tag(0) - - ZStack { - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: watchLayout.fontColorDark, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: watchLayout.majorTickColorDark, minorTickColor: watchLayout.minorTickColorDark, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection) - - Core(viewSize: size, compact: compact, dateString: chineseCalendar.hourString, timeString: chineseCalendar.quarterString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: watchLayout.innerColorDark, centerOffset: 0.05, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - .ignoresSafeArea() - .tag(1) - } - .onChange(of: selectedPageIndex) { _ in - hideDots = false - DualWatch.timer?.invalidate() - DualWatch.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in - hideDots = true - } - } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: hideDots ? .never : .automatic)) - } - } -} diff --git a/Watch/WatchLayout.swift b/Watch/WatchLayout.swift deleted file mode 100644 index 8731643..0000000 --- a/Watch/WatchLayout.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// WatchLayout.swift -// Chinese Time -// -// Created by Leo Liu on 5/11/23. -// - -import SwiftUI - -final class WatchLayout: MetaWatchLayout, ObservableObject { - static var shared: WatchLayout = .init() - - var textFont: UIFont - var centerFont: UIFont - @Published var refresh = false - - override init() { - textFont = UIFont.systemFont(ofSize: 10, weight: .regular) - centerFont = UIFont(name: "SourceHanSansKR-Heavy", size: 10)! - super.init() - } - - override func update(from str: String) { - super.update(from: str) - refresh.toggle() - } -} diff --git a/Watch/en.lproj/Chinese-Time-Watch-InfoPlist.strings b/Watch/en.lproj/Chinese-Time-Watch-InfoPlist.strings deleted file mode 100644 index f07b460..0000000 --- a/Watch/en.lproj/Chinese-Time-Watch-InfoPlist.strings +++ /dev/null @@ -1,15 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "Chinese Time"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/Watch/en.lproj/ChineseTime Watch App-InfoPlist.strings b/Watch/en.lproj/ChineseTime Watch App-InfoPlist.strings deleted file mode 100644 index a47e358..0000000 --- a/Watch/en.lproj/ChineseTime Watch App-InfoPlist.strings +++ /dev/null @@ -1,3 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "Chinese Time"; - diff --git a/Watch/en.lproj/Localizable.strings b/Watch/en.lproj/Localizable.strings deleted file mode 100644 index 5c6fcbb..0000000 --- a/Watch/en.lproj/Localizable.strings +++ /dev/null @@ -1,21 +0,0 @@ -/* Default save file name */ -"Default" = "Default"; - -/* Split Date and Time */ -"分列日時" = "Split Date & Time"; - -/* Corner radius ratio */ -"圓角比例" = "Rounded Corner Ratio"; - -/* Hint for syncing between watch and phone */ -"更多設置請移步 iOS App,可於手機與手錶間自動同步" = "Proceed to iOS App for more settings, which will sync to Watch automatically"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - -/* App Name */ -"華曆" = "Chinese Time"; - -/* Change Time */ -"調時" = "Alternative Time"; - diff --git a/Watch/layout.txt b/Watch/layout.txt index 65d7971..256995c 100644 --- a/Watch/layout.txt +++ b/Watch/layout.txt @@ -1,5 +1,7 @@ -globalMonth: true +globalMonth: false apparentTime: false +locationEnabled: true +dualWatch: false backAlpha: 1.0 firstRing: locations: 0.0, 0.25, 0.5, 0.75, 0.875; colors: 0xFF95517A, 0xFF9060BC, 0xFF6E68E7, 0xFF00A6FF, 0xFF7556EF; loop: true secondRing: locations: 0.0, 0.25, 0.5, 0.75; colors: 0xFF8658C5, 0xFF6E68E7, 0xFF5283EF, 0xFF6E68E7; loop: true @@ -10,10 +12,10 @@ majorTickAlpha: 0.0 minorTickColor: 0x00000000 minorTickAlpha: 0.7 fontColor: 0xFFFFFFFF -centerFontColor: locations: 0.25, 0.75; colors: 0xFF665598, 0xFFA5698A; loop: false +centerFontColor: locations: 0.25, 0.75; colors: 0xFFA36497, 0xFFC28F8A; loop: false evenSolarTermTickColor: 0xFF000000 oddSolarTermTickColor: 0xFF555555 -innerColorDark: 0xFF000000 +innerColorDark: 0xFF101010 majorTickColorDark: 0x00000000 minorTickColorDark: 0x007F7F7F fontColorDark: 0xFF000000 @@ -27,9 +29,10 @@ evenStermIndicator: 0xFFFFFFFF sunPositionIndicator: 0xFF000000, 0xFFAAADFF, 0xFF0A36B1, 0xFF6EBCD3 moonPositionIndicator: 0xFFE6B8AF, 0xFFE56572, 0xFF9E1E67 shadeAlpha: 0.25 -centerTextOffset: 0.1 -centerTextHorizontalOffset: 0.0 -verticalTextOffset: -0.5 +shadowSize: 0.03 +centerTextOffset: 0.05 +centerTextHorizontalOffset: 0.05 +verticalTextOffset: 0.0 horizontalTextOffset: 0.0 watchWidth: 396.0 watchHeight: 484.0 diff --git a/Watch/watchApp.swift b/Watch/watchApp.swift new file mode 100644 index 0000000..a8bddfc --- /dev/null +++ b/Watch/watchApp.swift @@ -0,0 +1,44 @@ +// +// ChineseTimeApp.swift +// ChineseTime Watch App +// +// Created by Leo Liu on 5/3/23. +// + +import SwiftUI + +@main +struct ChineseTimeWatchApp: App { + let watchConnectivity = WatchConnectivityManager.shared + let watchLayout = WatchLayout.shared + let watchSetting = WatchSetting.shared + let locationManager = LocationManager.shared + let chineseCalendar = ChineseCalendar(time: .now, compact: true) + let timer = Timer.publish(every: ChineseCalendar.updateInterval, on: .main, in: .common).autoconnect() + + init() { + let modelContext = ThemeData.container.mainContext + watchLayout.loadDefault(context: modelContext) + locationManager.requestLocation() + } + + var body: some Scene { + WindowGroup { + ContentView() + .modelContainer(ThemeData.container) + .environment(\.chineseCalendar, chineseCalendar) + .task { + self.update() + await updateCountDownRelevantIntents(chineseCalendar: chineseCalendar.copy) + } + .onReceive(timer) { _ in + self.update() + } + } + } + + func update() { + chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, + location: locationManager.location ?? watchLayout.location) + } +} diff --git a/Watch/zh-Hans.lproj/Chinese-Time-Watch-InfoPlist.strings b/Watch/zh-Hans.lproj/Chinese-Time-Watch-InfoPlist.strings deleted file mode 100644 index e966d2d..0000000 --- a/Watch/zh-Hans.lproj/Chinese-Time-Watch-InfoPlist.strings +++ /dev/null @@ -1,15 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "华历"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/Watch/zh-Hans.lproj/ChineseTime Watch App-InfoPlist.strings b/Watch/zh-Hans.lproj/ChineseTime Watch App-InfoPlist.strings deleted file mode 100644 index beca3b2..0000000 --- a/Watch/zh-Hans.lproj/ChineseTime Watch App-InfoPlist.strings +++ /dev/null @@ -1,3 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "华历"; - diff --git a/Watch/zh-Hans.lproj/Localizable.strings b/Watch/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 0d4e673..0000000 --- a/Watch/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,21 +0,0 @@ -/* Default save file name */ -"Default" = "常备"; - -/* Split Date and Time */ -"分列日時" = "分列日时"; - -/* Corner radius ratio */ -"圓角比例" = "圆角比例"; - -/* Hint for syncing between watch and phone */ -"更多設置請移步 iOS App,可於手機與手錶間自動同步" = "更多设置请移步手机App,可于手机与手表间自动同步"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - -/* App Name */ -"華曆" = "华历"; - -/* Change Time */ -"調時" = "调时"; - diff --git a/Watch/zh-Hant.lproj/Chinese-Time-Watch-InfoPlist.strings b/Watch/zh-Hant.lproj/Chinese-Time-Watch-InfoPlist.strings deleted file mode 100644 index 9caf5d5..0000000 --- a/Watch/zh-Hant.lproj/Chinese-Time-Watch-InfoPlist.strings +++ /dev/null @@ -1,6 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "華曆"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - diff --git a/Watch/zh-Hant.lproj/ChineseTime Watch App-InfoPlist.strings b/Watch/zh-Hant.lproj/ChineseTime Watch App-InfoPlist.strings deleted file mode 100644 index 824a00f..0000000 --- a/Watch/zh-Hant.lproj/ChineseTime Watch App-InfoPlist.strings +++ /dev/null @@ -1,3 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "華曆"; - diff --git a/Watch/zh-Hant.lproj/Localizable.strings b/Watch/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 3a05a01..0000000 --- a/Watch/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,6 +0,0 @@ -/* Default save file name */ -"Default" = "常備"; - -/* Hint for syncing between watch and phone */ -"更多設置請移步 iOS App,可於手機與手錶間自動同步" = "更多設置請移步手機 App,可於手機與手錶間自動同步"; - diff --git a/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..e12e779 100644 --- a/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/WatchWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,10 @@ { "colors" : [ { + "color" : { + "platform" : "universal", + "reference" : "systemPinkColor" + }, "idiom" : "universal" } ], diff --git a/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 49c81cd..0000000 --- a/WatchWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "watchos", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WatchWidget/Assets.xcassets/Contents.json b/WatchWidget/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/WatchWidget/Assets.xcassets/Contents.json +++ b/WatchWidget/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/WatchWidget/Base.lproj/WatchWidget.intentdefinition b/WatchWidget/Base.lproj/WatchWidget.intentdefinition deleted file mode 100644 index 42ccc87..0000000 --- a/WatchWidget/Base.lproj/WatchWidget.intentdefinition +++ /dev/null @@ -1,378 +0,0 @@ - - - - - INEnums - - - INEnumDisplayName - Circular Mode - INEnumDisplayNameID - nsPERY - INEnumGeneratesHeader - - INEnumName - CircularMode - INEnumType - Regular - INEnumValues - - - INEnumValueDisplayName - Default - INEnumValueDisplayNameID - LF5EdZ - INEnumValueName - unknown - - - INEnumValueDisplayName - Sun & Moon - INEnumValueDisplayNameID - Q39il4 - INEnumValueIndex - 1 - INEnumValueName - daylight - - - INEnumValueDisplayName - Month & Day - INEnumValueDisplayNameID - b7SqPZ - INEnumValueIndex - 2 - INEnumValueName - monthDay - - - - - INEnumDisplayName - Curve Mode - INEnumDisplayNameID - 0F5msv - INEnumGeneratesHeader - - INEnumName - CurveMode - INEnumType - Regular - INEnumValues - - - INEnumValueDisplayName - Default - INEnumValueDisplayNameID - j6yiGJ - INEnumValueName - unknown - - - INEnumValueDisplayName - Solar Terms - INEnumValueDisplayNameID - rNmT7n - INEnumValueIndex - 1 - INEnumValueName - solarTerms - - - INEnumValueDisplayName - Lunar Phases - INEnumValueDisplayNameID - 4GqETF - INEnumValueIndex - 2 - INEnumValueName - lunarPhases - - - INEnumValueDisplayName - Sunrise/set - INEnumValueDisplayNameID - NjcTZd - INEnumValueIndex - 3 - INEnumValueName - sunriseSet - - - INEnumValueDisplayName - Moonrise/set - INEnumValueDisplayNameID - axjc3N - INEnumValueIndex - 4 - INEnumValueName - moonriseSet - - - - - INIntentDefinitionModelVersion - 1.2 - INIntentDefinitionNamespace - 88xZPY - INIntentDefinitionSystemVersion - 22E261 - INIntentDefinitionToolsBuildVersion - 14E222b - INIntentDefinitionToolsVersion - 14.3 - INIntents - - - INIntentCategory - information - INIntentClassName - SingleLineIntent - INIntentDescription - Single line widget options - INIntentDescriptionID - tVvJ9c - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentName - SingleLine - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Single Line - INIntentTitleID - gpCwrM - INIntentType - Custom - INIntentVerb - View - - - INIntentCategory - information - INIntentClassName - CircularIntent - INIntentDescription - Circular widget options - INIntentDescriptionID - uKd3Ry - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 2 - INIntentName - Circular - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - Circluar Mode - INIntentParameterDisplayNameID - 6jXf0i - INIntentParameterDisplayPriority - 1 - INIntentParameterEnumType - CircularMode - INIntentParameterEnumTypeNamespace - 88xZPY - INIntentParameterName - mode - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${mode}’. - INIntentParameterPromptDialogFormatStringID - CX2NbA - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${mode}’? - INIntentParameterPromptDialogFormatStringID - 8jLN0l - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsResolution - - INIntentParameterTag - 2 - INIntentParameterType - Integer - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Circular - INIntentTitleID - Ymgolg - INIntentType - Custom - INIntentVerb - View - - - INIntentCategory - information - INIntentClassName - CurveIntent - INIntentDescription - Curve widget options - INIntentDescriptionID - 1StuGp - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 2 - INIntentName - Curve - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterCustomDisambiguation - - INIntentParameterDisplayName - Curve Mode - INIntentParameterDisplayNameID - ZAfzGO - INIntentParameterDisplayPriority - 1 - INIntentParameterEnumType - CurveMode - INIntentParameterEnumTypeNamespace - 88xZPY - INIntentParameterName - target - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${target}’. - INIntentParameterPromptDialogFormatStringID - pIy9tn - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${target}’? - INIntentParameterPromptDialogFormatStringID - rTHEbQ - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterSupportsResolution - - INIntentParameterTag - 2 - INIntentParameterType - Integer - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Curve - INIntentTitleID - oDF3vf - INIntentType - Custom - INIntentVerb - View - - - INTypes - - - diff --git a/WatchWidget/Info.plist b/WatchWidget/Info.plist index 2092828..27799cf 100644 --- a/WatchWidget/Info.plist +++ b/WatchWidget/Info.plist @@ -4,6 +4,8 @@ ITSAppUsesNonExemptEncryption + LSHasLocalizedDisplayName + NSExtension NSExtensionPointIdentifier diff --git a/WatchWidget/WatchWidgetExtension.entitlements b/WatchWidget/WatchWidget.entitlements similarity index 100% rename from WatchWidget/WatchWidgetExtension.entitlements rename to WatchWidget/WatchWidget.entitlements diff --git a/WatchWidget/WatchWidget.swift b/WatchWidget/WatchWidget.swift deleted file mode 100644 index a980648..0000000 --- a/WatchWidget/WatchWidget.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// WatchWidget.swift -// WatchWidget -// -// Created by Leo Liu on 5/10/23. -// - -import Intents -import SwiftUI -import WidgetKit - -struct LineProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> LineEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - return LineEntry(date: Date(), configuration: SingleLineIntent(), chinsesCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: SingleLineIntent, in context: Context, completion: @escaping (LineEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - let entry = LineEntry(date: Date(), configuration: configuration, chinsesCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: SingleLineIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [LineEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for timeOffset in 0 ..< 12 { - let entryDate = Calendar.current.date(byAdding: .hour, value: timeOffset, to: currentDate)! - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = LineEntry(date: entryDate, configuration: configuration, chinsesCalendar: chineseCalendar.copy) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - func recommendations() -> [IntentRecommendation] { - return [ - IntentRecommendation(intent: SingleLineIntent(), description: "Single Line Widget") - ] - } -} - -struct LineEntry: TimelineEntry { - let date: Date - let configuration: SingleLineIntent - let chinsesCalendar: ChineseCalendar -} - -struct LineEntryView: View { - var entry: LineProvider.Entry - - var body: some View { - LineDescription(chineseCalendar: entry.chinsesCalendar) - } -} - -struct LineWidget: Widget { - let kind: String = "LineWidget" - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: SingleLineIntent.self, provider: LineProvider()) { entry in - LineEntryView(entry: entry) - } - .configurationDisplayName("Single Line") - .description("Single line date and time widget.") - .supportedFamilies([.accessoryInline]) - } -} - -struct CircularProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> CircularEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - return CircularEntry(date: Date(), configuration: CircularIntent(), chinsesCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: CircularIntent, in context: Context, completion: @escaping (CircularEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - let entry = CircularEntry(date: Date(), configuration: configuration, chinsesCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: CircularIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [CircularEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for timeOffset in 0 ..< 12 { - let entryDate: Date - switch configuration.mode { - case .monthDay: - entryDate = Calendar.current.date(byAdding: .hour, value: timeOffset * 6, to: currentDate)! - default: - entryDate = Calendar.current.date(byAdding: .hour, value: timeOffset, to: currentDate)! - } - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = CircularEntry(date: entryDate, configuration: configuration, chinsesCalendar: chineseCalendar.copy) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - func recommendations() -> [IntentRecommendation] { - let daylight = CircularIntent() - daylight.mode = .daylight - let monthDay = CircularIntent() - monthDay.mode = .monthDay - return [ - IntentRecommendation(intent: daylight, description: "日月光華"), - IntentRecommendation(intent: monthDay, description: "歲月之輪") - ] - } -} - -struct CircularEntry: TimelineEntry { - let date: Date - let configuration: CircularIntent - let chinsesCalendar: ChineseCalendar -} - -struct CircularEntryView: View { - var entry: CircularProvider.Entry - - func sunTimes(times: [ChineseCalendar.NamedPosition?]) -> (start: CGFloat, end: CGFloat)? { - guard times.count == 5 else { return nil } - if let sunrise = times[1]?.pos, let sunset = times[3]?.pos { - return (start: CGFloat(sunrise), end: CGFloat(sunset)) - } else if times[1] == nil && times[3] == nil { - if let _ = times[2]?.pos { - return (start: 0, end: 1) - } else { - return (start: 0, end: 1e-7) - } - } else { - if let sunrise = times[1]?.pos { - return (start: CGFloat(sunrise), end: 1.0) - } else if let sunset = times[3]?.pos { - return (start: 0, end: CGFloat(sunset)) - } else { - return (start: 0, end: 1e-7) - } - } - } - - func moonTimes(times: [ChineseCalendar.NamedPosition?]) -> ((start: CGFloat, end: CGFloat)?, CGFloat?) { - guard times.count == 6 else { return (nil, nil) } - if let firstMoonRise = times[0]?.pos { - if let firstMoonSet = times[2]?.pos { - return ((start: CGFloat(firstMoonRise), end: CGFloat(firstMoonSet)), times[1].flatMap { CGFloat($0.pos) }) - } else { - return ((start: CGFloat(firstMoonRise), end: 1.0), times[1].flatMap { CGFloat($0.pos) } ?? 1.0) - } - } else if let secondMoonSet = times[5]?.pos { - if let secondMoonRise = times[3]?.pos { - return ((start: CGFloat(secondMoonRise), end: CGFloat(secondMoonSet)), times[4].flatMap { CGFloat($0.pos) }) - } else { - return ((start: 0.0, end: CGFloat(secondMoonSet)), times[4].flatMap { CGFloat($0.pos) } ?? 0.0) - } - } else if let firstMoonSet = times[2]?.pos, let secondMoonRise = times[3]?.pos { - return ((start: CGFloat(secondMoonRise), end: CGFloat(firstMoonSet)), (times[1] ?? times[4]).flatMap { CGFloat($0.pos) }) - } else { - if let firstMoonSet = times[2]?.pos { - return ((start: 0, end: CGFloat(firstMoonSet)), times[1].flatMap { CGFloat($0.pos) } ?? 0.0) - } else if let secondMoonRise = times[3]?.pos { - return ((start: CGFloat(secondMoonRise), end: 1.0), times[4].flatMap { CGFloat($0.pos) } ?? 1.0) - } else { - if times[1] != nil || times[4] != nil { - return ((start: 0, end: 1), nil) - } else { - return ((start: 0, end: 1e-7), nil) - } - } - } - } - - var body: some View { - let layout = WatchLayout.shared - let phase = StartingPhase() - switch entry.configuration.mode { - case .monthDay: - let outerGradient = applyGradient(gradient: layout.firstRing, startingAngle: phase.firstRing) - let innerGradient = applyGradient(gradient: layout.secondRing, startingAngle: phase.secondRing) - Circular(outer: (start: phase.firstRing, end: entry.chinsesCalendar.currentDayInYear + phase.firstRing), - inner: (start: phase.secondRing, end: entry.chinsesCalendar.currentDayInMonth + phase.secondRing), - outerGradient: outerGradient, innerGradient: innerGradient) - .widgetLabel { - Text(String(entry.chinsesCalendar.dateString.reversed())) - } - default: - let outerGradient = applyGradient(gradient: layout.thirdRing, startingAngle: phase.thirdRing) - let innerGradient = applyGradient(gradient: layout.secondRing, startingAngle: phase.secondRing) - let (inner, innerDirection) = moonTimes(times: entry.chinsesCalendar.sunMoonPositions.lunar) - if let outer = sunTimes(times: entry.chinsesCalendar.sunMoonPositions.solar), let inner = inner { - Circular(outer: (start: outer.start + phase.thirdRing, end: outer.end + phase.thirdRing), - inner: (start: inner.start + phase.secondRing, end: inner.end + phase.secondRing), - current: entry.chinsesCalendar.currentHourInDay, - innerDirection: innerDirection, - outerGradient: outerGradient, innerGradient: innerGradient, - currentColor: Color(cgColor: layout.thirdRing.interpolate(at: entry.chinsesCalendar.currentHourInDay))) - .widgetLabel { - Text(String((entry.chinsesCalendar.hourString + entry.chinsesCalendar.shortQuarterString).reversed())) - } - } else { - Circular(outer: (start: 0, end: 1e-7), inner: (start: 0, end: 1e-7), - current: entry.chinsesCalendar.currentHourInDay, outerGradient: outerGradient, innerGradient: innerGradient, - currentColor: Color(cgColor: layout.thirdRing.interpolate(at: entry.chinsesCalendar.currentHourInDay))) - .widgetLabel { - Text(String((entry.chinsesCalendar.hourString + entry.chinsesCalendar.shortQuarterString).reversed())) - } - } - } - } -} - -struct CircularWidget: Widget { - let kind: String = "Circular" - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: CircularIntent.self, provider: CircularProvider()) { entry in - CircularEntryView(entry: entry) - } - .configurationDisplayName("Circular") - .description("Circular View.") -#if os(watchOS) - .supportedFamilies([.accessoryCircular, .accessoryCorner]) -#else - .supportedFamilies([.accessoryCircular]) -#endif - } -} diff --git a/WatchWidget/WatchWidgetBundle.swift b/WatchWidget/WatchWidgetBundle.swift index cd598e6..8d82caf 100644 --- a/WatchWidget/WatchWidgetBundle.swift +++ b/WatchWidget/WatchWidgetBundle.swift @@ -6,216 +6,15 @@ // import SwiftUI -import WidgetKit @main struct WatchWidgetBundle: WidgetBundle { - init() { - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - } - @WidgetBundleBuilder var body: some Widget { LineWidget() CircularWidget() CurveWidget() - } -} - -struct Curve: View { - enum Icon { - case solarTerm(view: SolarTerm) - case moon(view: MoonPhase) - case sunrise(view: Sun) - } - - @State var size: CGSize = .zero - var icon: Icon - var barColor: Color - var start: Date? - var end: Date? - - var body: some View { - GeometryReader { proxy in - ZStack { - switch icon { - case .solarTerm(view: let view): - view - case .moon(view: let view): - view - case .sunrise(view: let view): - view - } - } - .frame(width: size.width, height: size.height) - .widgetLabel { - if let start = start, let end = end { - ProgressView(timerInterval: start ... end, countsDown: false) { - Text(end, style: .relative) - } - .tint(barColor) - } - } - .onAppear { - size = proxy.size - } - } - } -} - -struct CurveProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> CurveEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - return CurveEntry(date: Date(), configuration: CurveIntent(), chinsesCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: CurveIntent, in context: Context, completion: @escaping (CurveEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - let entry = CurveEntry(date: Date(), configuration: configuration, chinsesCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: CurveIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [CurveEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for timeOffset in 0 ..< 12 { - let entryDate: Date - switch configuration.target { - case .moonriseSet, .sunriseSet: - entryDate = Calendar.current.date(byAdding: .hour, value: timeOffset, to: currentDate)! - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - default: - let targetDate = Calendar.current.date(byAdding: .day, value: timeOffset, to: currentDate)! - chineseCalendar.update(time: targetDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - entryDate = chineseCalendar.startOfNextDay - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - } - - let entry = CurveEntry(date: entryDate, configuration: configuration, chinsesCalendar: chineseCalendar.copy) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - func recommendations() -> [IntentRecommendation] { - let solarTerms = { let intent = CurveIntent(); intent.target = .solarTerms; return intent }() - let lunarPhases = { let intent = CurveIntent(); intent.target = .lunarPhases; return intent }() - let sunriseSet = { let intent = CurveIntent(); intent.target = .sunriseSet; return intent }() - let moonriseSet = { let intent = CurveIntent(); intent.target = .moonriseSet; return intent }() - - return [ - IntentRecommendation(intent: solarTerms, description: "次節氣"), - IntentRecommendation(intent: lunarPhases, description: "次朔望"), - IntentRecommendation(intent: sunriseSet, description: "日出入"), - IntentRecommendation(intent: moonriseSet, description: "月出入") - ] - } -} - -struct CurveEntry: TimelineEntry { - let date: Date - let configuration: CurveIntent - let chinsesCalendar: ChineseCalendar -} - -struct CurveEntryView: View { - var entry: CurveProvider.Entry - - private func find(in dates: [ChineseCalendar.NamedDate], at date: Date) -> (previous: ChineseCalendar.NamedDate?, next: ChineseCalendar.NamedDate?) { - if dates.count > 1 { - let atDate = ChineseCalendar.NamedDate(name: "", date: date) - let index = dates.insertionIndex(of: atDate, comparison: { $0.date < $1.date }) - if index > 0 && index < dates.count { - return (previous: dates[index - 1], next: dates[index]) - } else if index < dates.count { - return (previous: nil, next: dates[index]) - } else { - return (previous: dates[index - 1], next: nil) - } - } else { - return (previous: nil, next: nil) - } - } - - private func findSolarTerm(_ solarTerm: String) -> Int { - if let even = ChineseCalendar.evenSolarTermChinese.firstIndex(of: solarTerm) { - return even * 2 - } else if let odd = ChineseCalendar.oddSolarTermChinese.firstIndex(of: solarTerm) { - return odd * 2 + 1 - } else { - return -1 - } - } - - var body: some View { - let layout = WatchLayout.shared - switch entry.configuration.target { - case .lunarPhases: - let (previous, next) = find(in: entry.chinsesCalendar.moonPhases, at: entry.chinsesCalendar.time) - if let next = next, let previous = previous { - let color = next.name == ChineseCalendar.MoonPhases[0] ? layout.eclipseIndicator : layout.fullmoonIndicator - Curve(icon: .moon(view: MoonPhase(angle: next.name == ChineseCalendar.MoonPhases[0] ? 0.05 : 0.5, color: color)), - barColor: Color(cgColor: layout.secondRing.interpolate(at: entry.chinsesCalendar.currentDayInMonth)), - start: previous.date, end: next.date) - } - case .sunriseSet: - let sunriseAndSet = entry.chinsesCalendar.sunTimes.filter { $0.name == ChineseCalendar.dayTimeName[1] || $0.name == ChineseCalendar.dayTimeName[3] } - let (previous, next) = find(in: sunriseAndSet, at: entry.chinsesCalendar.time) - if let next = next, let previous = previous { - let color = next.name == ChineseCalendar.dayTimeName[1] ? layout.sunPositionIndicator[1] : layout.sunPositionIndicator[3] - Curve(icon: .sunrise(view: Sun(color: color, rise: next.name == ChineseCalendar.dayTimeName[1])), - barColor: Color(cgColor: layout.thirdRing.interpolate(at: entry.chinsesCalendar.currentHourInDay)), - start: previous.date, end: next.date) - } else { - Curve(icon: .sunrise(view: Sun(color: layout.sunPositionIndicator[2], rise: nil)), - barColor: Color(cgColor: layout.thirdRing.interpolate(at: entry.chinsesCalendar.currentHourInDay)), - start: nil, end: nil) - } - case .moonriseSet: - let moonriseAndSet = entry.chinsesCalendar.moonTimes.filter { $0.name == ChineseCalendar.moonTimeName[0] || $0.name == ChineseCalendar.moonTimeName[2] } - let (previous, next) = find(in: moonriseAndSet, at: entry.chinsesCalendar.time) - if let next = next, let previous = previous { - let color = next.name == ChineseCalendar.moonTimeName[0] ? layout.moonPositionIndicator[0] : layout.moonPositionIndicator[2] - Curve(icon: .moon(view: MoonPhase(angle: entry.chinsesCalendar.currentDayInMonth, color: color, rise: next.name == ChineseCalendar.moonTimeName[0])), - barColor: Color(cgColor: layout.secondRing.interpolate(at: entry.chinsesCalendar.currentDayInMonth)), - start: previous.date, end: next.date) - } else { - Curve(icon: .moon(view: MoonPhase(angle: entry.chinsesCalendar.currentDayInMonth, color: layout.moonPositionIndicator[1], rise: nil)), - barColor: Color(cgColor: layout.thirdRing.interpolate(at: entry.chinsesCalendar.currentDayInMonth)), - start: nil, end: nil) - } - default: - let (previous, next) = find(in: entry.chinsesCalendar.solarTerms, at: entry.chinsesCalendar.time) - if let next = next, let previous = previous { - let color = ChineseCalendar.evenSolarTermChinese.contains(next.name) ? layout.evenStermIndicator : layout.oddStermIndicator - let index = findSolarTerm(next.name) - if index >= 0 { - Curve(icon: .solarTerm(view: SolarTerm(angle: CGFloat(index) / 24.0, color: color)), - barColor: Color(cgColor: layout.firstRing.interpolate(at: entry.chinsesCalendar.currentDayInYear)), - start: previous.date, end: next.date) - } - } - } - } -} - -struct CurveWidget: Widget { - let kind: String = "Curve" - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: CurveIntent.self, provider: CurveProvider()) { entry in - CurveEntryView(entry: entry) - } - .configurationDisplayName("Curve") - .description("Curve View.") - .supportedFamilies([.accessoryCorner]) + RectWidget() + DateCardWidget() } } diff --git a/WatchWidget/WatchWidgetView.swift b/WatchWidget/WatchWidgetView.swift deleted file mode 100644 index e645388..0000000 --- a/WatchWidget/WatchWidgetView.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// WatchWidgetFace.swift -// WatchWidgetExtension -// -// Created by Leo Liu on 5/11/23. -// - -import SwiftUI - -struct LineDescription: View { - @State var text: String - - init(chineseCalendar: ChineseCalendar) { - var text = chineseCalendar.dateString - for solarTerm in chineseCalendar.eventInDay.oddSolarTerm { - text += "・\(solarTerm.name)" - } - for solarTerm in chineseCalendar.eventInDay.evenSolarTerm { - text += "・\(solarTerm.name)" - } - for moon in chineseCalendar.eventInDay.eclipse { - text += "・\(moon.name)" - } - for moon in chineseCalendar.eventInDay.fullMoon { - text += "・\(moon.name)" - } - text += "・\(chineseCalendar.hourString)" - self.text = text - } - - var body: some View { - Text(String(text.reversed())) - } -} - -struct CircularLine: View { - var lineWidth: CGFloat - var start: CGFloat - var end: CGFloat - - var body: some View { - let length = end > start ? end - start : end - start + 1 - - ZStack { - Circle() - .stroke( - Color.white.opacity(0.5), - lineWidth: lineWidth - ) - .padding(lineWidth / 2) - Circle() - .trim(from: 0, to: length) - .stroke( - Color.white, - style: StrokeStyle( - lineWidth: lineWidth, - lineCap: .round - ) - ) - .padding(lineWidth / 2) - .rotationEffect(.radians(CGFloat.pi * 2.0 * (0.25 + start))) - .scaleEffect(CGSize(width: -1, height: 1)) - } - } -} - -struct Circular: View { - @State var size: CGSize = .zero - var outer: (start: CGFloat, end: CGFloat) - var inner: (start: CGFloat, end: CGFloat) - var current: CGFloat? - var innerDirection: CGFloat? - var outerGradient: Gradient - var innerGradient: Gradient - var currentColor: Color? - - var body: some View { - GeometryReader { proxy in - ZStack { - AngularGradient(gradient: outerGradient, center: .center, angle: .degrees(90)).mask { - CircularLine(lineWidth: min(size.width, size.height) * 0.1, start: outer.start, end: outer.end) - } - AngularGradient(gradient: innerGradient, center: .center, angle: .radians((-0.25 - (innerDirection ?? 0.5)) * CGFloat.pi * 2.0)).mask { - CircularLine(lineWidth: min(size.width, size.height) * 0.1, start: inner.start, end: inner.end) - .frame(width: size.width * 0.7, height: size.height * 0.7) - } - if let current = current, let currentColor = currentColor { - currentColor - .clipShape(Capsule()) - .frame(width: size.width * 0.28, height: min(size.width, size.height) * 0.1) - .position(CGPoint(x: size.width * 0.13, y: size.height / 2)) - .rotationEffect(.radians((current - 0.25) * CGFloat.pi * 2.0)) - .scaleEffect(CGSize(width: -1, height: 1)) - .shadow(color: .black, radius: min(size.width, size.height) * 0.05) - } - } - .onAppear { - size = proxy.size - } - } - } -} diff --git a/WatchWidget/en.lproj/InfoPlist.strings b/WatchWidget/en.lproj/InfoPlist.strings deleted file mode 100644 index dfdcf48..0000000 --- a/WatchWidget/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "Chinese Time Widget"; - -/* Bundle name */ -"CFBundleName" = "Watch Widget Extension"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/WatchWidget/en.lproj/Localizable.strings b/WatchWidget/en.lproj/Localizable.strings deleted file mode 100644 index 37bdd87..0000000 --- a/WatchWidget/en.lproj/Localizable.strings +++ /dev/null @@ -1,45 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "Circular"; - -/* No comment provided by engineer. */ -"Circular View." = "You can choose from \"Sun & Moon\" and \"Month & Day\""; - -/* No comment provided by engineer. */ -"Curve" = "Countdown"; - -/* No comment provided by engineer. */ -"Curve View." = "You can choose from 4 categories to display count down"; - -/* Default save file name */ -"Default" = "Default"; - -/* No comment provided by engineer. */ -"Single Line" = "Single Line"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "Single line to display date and hour"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "Date and Hour String"; - -/* No comment provided by engineer. */ -"日出入" = "Sunrise & Sunset"; - -/* No comment provided by engineer. */ -"日月光華" = "Sunlight & Moonlight"; - -/* No comment provided by engineer. */ -"月出入" = "Moonrise & Moonset"; - -/* No comment provided by engineer. */ -"次朔望" = "Next New/Full Moon"; - -/* No comment provided by engineer. */ -"次節氣" = "Next Solar Term"; - -/* No comment provided by engineer. */ -"歲月之輪" = "Month and Day Rings"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - diff --git a/WatchWidget/en.lproj/WatchWidget.strings b/WatchWidget/en.lproj/WatchWidget.strings deleted file mode 100644 index aa1de82..0000000 --- a/WatchWidget/en.lproj/WatchWidget.strings +++ /dev/null @@ -1,102 +0,0 @@ -/* (No Comment) */ -"0F5msv" = "Countdown Mode"; - -/* (No Comment) */ -"1StuGp" = "Countdown widget options"; - -/* (No Comment) */ -"4GqETF" = "Lunar Phases"; - -/* (No Comment) */ -"6jXf0i" = "Circular Mode"; - -/* (No Comment) */ -"8jLN0l-b7SqPZ" = "Just to confirm, you wanted ‘Month & Day’?"; - -/* (No Comment) */ -"8jLN0l-LF5EdZ" = "Just to confirm, you wanted ‘Default’?"; - -/* (No Comment) */ -"8jLN0l-Q39il4" = "Just to confirm, you wanted ‘Sun & Moon’?"; - -/* (No Comment) */ -"axjc3N" = "Moonrise/set"; - -/* (No Comment) */ -"b7SqPZ" = "Month & Day"; - -/* (No Comment) */ -"CX2NbA-b7SqPZ" = "There are ${count} options matching ‘Month & Day’."; - -/* (No Comment) */ -"CX2NbA-LF5EdZ" = "There are ${count} options matching ‘Default’."; - -/* (No Comment) */ -"CX2NbA-Q39il4" = "There are ${count} options matching ‘Sun & Moon’."; - -/* (No Comment) */ -"gpCwrM" = "Single Line"; - -/* (No Comment) */ -"j6yiGJ" = "Default"; - -/* (No Comment) */ -"LF5EdZ" = "Default"; - -/* (No Comment) */ -"NjcTZd" = "Sunrise/set"; - -/* (No Comment) */ -"nsPERY" = "Circular Mode"; - -/* (No Comment) */ -"oDF3vf" = "Countdown"; - -/* (No Comment) */ -"pIy9tn-4GqETF" = "There are ${count} options matching ‘Lunar Phases’."; - -/* (No Comment) */ -"pIy9tn-axjc3N" = "There are ${count} options matching ‘Moonrise/set’."; - -/* (No Comment) */ -"pIy9tn-j6yiGJ" = "There are ${count} options matching ‘Default’."; - -/* (No Comment) */ -"pIy9tn-NjcTZd" = "There are ${count} options matching ‘Sunrise/set’."; - -/* (No Comment) */ -"pIy9tn-rNmT7n" = "There are ${count} options matching ‘Solar Terms’."; - -/* (No Comment) */ -"Q39il4" = "Sun & Moon"; - -/* (No Comment) */ -"rNmT7n" = "Solar Terms"; - -/* (No Comment) */ -"rTHEbQ-4GqETF" = "Just to confirm, you wanted ‘Lunar Phases’?"; - -/* (No Comment) */ -"rTHEbQ-axjc3N" = "Just to confirm, you wanted ‘Moonrise/set’?"; - -/* (No Comment) */ -"rTHEbQ-j6yiGJ" = "Just to confirm, you wanted ‘Default’?"; - -/* (No Comment) */ -"rTHEbQ-NjcTZd" = "Just to confirm, you wanted ‘Sunrise/set’?"; - -/* (No Comment) */ -"rTHEbQ-rNmT7n" = "Just to confirm, you wanted ‘Solar Terms’?"; - -/* (No Comment) */ -"tVvJ9c" = "Single line widget options"; - -/* (No Comment) */ -"uKd3Ry" = "Circular widget options"; - -/* (No Comment) */ -"Ymgolg" = "Circular"; - -/* (No Comment) */ -"ZAfzGO" = "Count Down Mode"; - diff --git a/WatchWidget/zh-Hans.lproj/InfoPlist.strings b/WatchWidget/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 4e1e594..0000000 --- a/WatchWidget/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "华历挂件"; - -/* Bundle name */ -"CFBundleName" = "手表挂件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/WatchWidget/zh-Hans.lproj/Localizable.strings b/WatchWidget/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 8cad9b5..0000000 --- a/WatchWidget/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,45 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "圆轮"; - -/* No comment provided by engineer. */ -"Circular View." = "可选“日月光华”或“岁月之轮”"; - -/* No comment provided by engineer. */ -"Curve" = "时计"; - -/* No comment provided by engineer. */ -"Curve View." = "可选四类目标倒计时"; - -/* Default save file name */ -"Default" = "常备"; - -/* No comment provided by engineer. */ -"Single Line" = "纯文字"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "单栏文字展示日、时"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "日、时文字"; - -/* No comment provided by engineer. */ -"日出入" = "日出入"; - -/* No comment provided by engineer. */ -"日月光華" = "日月光华"; - -/* No comment provided by engineer. */ -"月出入" = "月出入"; - -/* No comment provided by engineer. */ -"次朔望" = "次朔望"; - -/* No comment provided by engineer. */ -"次節氣" = "次节气"; - -/* No comment provided by engineer. */ -"歲月之輪" = "岁月之轮"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - diff --git a/WatchWidget/zh-Hans.lproj/WatchWidget.strings b/WatchWidget/zh-Hans.lproj/WatchWidget.strings deleted file mode 100644 index e64994c..0000000 --- a/WatchWidget/zh-Hans.lproj/WatchWidget.strings +++ /dev/null @@ -1,102 +0,0 @@ -/* (No Comment) */ -"0F5msv" = "时计选项"; - -/* (No Comment) */ -"1StuGp" = "时计挂件选项"; - -/* (No Comment) */ -"4GqETF" = "月相"; - -/* (No Comment) */ -"6jXf0i" = "圆轮选项"; - -/* (No Comment) */ -"8jLN0l-b7SqPZ" = "确认选‘岁月之轮’吗?"; - -/* (No Comment) */ -"8jLN0l-LF5EdZ" = "确认选‘默认’吗?"; - -/* (No Comment) */ -"8jLN0l-Q39il4" = "确认选‘日月光华’吗?"; - -/* (No Comment) */ -"axjc3N" = "月出入"; - -/* (No Comment) */ -"b7SqPZ" = "岁月之轮"; - -/* (No Comment) */ -"CX2NbA-b7SqPZ" = "共有${count}种‘岁月之轮’。"; - -/* (No Comment) */ -"CX2NbA-LF5EdZ" = "共有${count}种‘默认’。"; - -/* (No Comment) */ -"CX2NbA-Q39il4" = "共有${count}种‘日月光华’。"; - -/* (No Comment) */ -"gpCwrM" = "纯文字"; - -/* (No Comment) */ -"j6yiGJ" = "默认"; - -/* (No Comment) */ -"LF5EdZ" = "默认"; - -/* (No Comment) */ -"NjcTZd" = "日出入"; - -/* (No Comment) */ -"nsPERY" = "圆轮选项"; - -/* (No Comment) */ -"oDF3vf" = "时计"; - -/* (No Comment) */ -"pIy9tn-4GqETF" = "共有${count}种‘月相’。"; - -/* (No Comment) */ -"pIy9tn-axjc3N" = "共有${count}种‘月出入’。"; - -/* (No Comment) */ -"pIy9tn-j6yiGJ" = "共有${count}种‘默认’。"; - -/* (No Comment) */ -"pIy9tn-NjcTZd" = "共有${count}种‘日出入’。"; - -/* (No Comment) */ -"pIy9tn-rNmT7n" = "共有${count}种‘节气’。"; - -/* (No Comment) */ -"Q39il4" = "日月光华"; - -/* (No Comment) */ -"rNmT7n" = "节气"; - -/* (No Comment) */ -"rTHEbQ-4GqETF" = "确认选‘月相’吗?"; - -/* (No Comment) */ -"rTHEbQ-axjc3N" = "确认选‘月出入’吗?"; - -/* (No Comment) */ -"rTHEbQ-j6yiGJ" = "确认选‘默认’吗?"; - -/* (No Comment) */ -"rTHEbQ-NjcTZd" = "确认选‘日出入’吗?"; - -/* (No Comment) */ -"rTHEbQ-rNmT7n" = "确认选‘节气’吗?"; - -/* (No Comment) */ -"tVvJ9c" = "纯文字挂件选项"; - -/* (No Comment) */ -"uKd3Ry" = "圆轮挂件选项"; - -/* (No Comment) */ -"Ymgolg" = "圆轮"; - -/* (No Comment) */ -"ZAfzGO" = "时计选项"; - diff --git a/WatchWidget/zh-Hant.lproj/InfoPlist.strings b/WatchWidget/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index c61e9e4..0000000 --- a/WatchWidget/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "華曆掛件"; - -/* Bundle name */ -"CFBundleName" = "手錶掛件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - diff --git a/WatchWidget/zh-Hant.lproj/Localizable.strings b/WatchWidget/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 657e263..0000000 --- a/WatchWidget/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,24 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "圓輪"; - -/* No comment provided by engineer. */ -"Circular View." = "可選「日月光華」或「歲月之輪」"; - -/* No comment provided by engineer. */ -"Curve" = "時計"; - -/* No comment provided by engineer. */ -"Curve View." = "可選四類目標倒計時"; - -/* Default save file name */ -"Default" = "常備"; - -/* No comment provided by engineer. */ -"Single Line" = "純文字"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "單欄文字展示日、時"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "日、時文字"; - diff --git a/WatchWidget/zh-Hant.lproj/WatchWidget.strings b/WatchWidget/zh-Hant.lproj/WatchWidget.strings deleted file mode 100644 index 2b36bf1..0000000 --- a/WatchWidget/zh-Hant.lproj/WatchWidget.strings +++ /dev/null @@ -1,102 +0,0 @@ -/* (No Comment) */ -"0F5msv" = "時計選項"; - -/* (No Comment) */ -"1StuGp" = "時計掛件選項"; - -/* (No Comment) */ -"4GqETF" = "月相"; - -/* (No Comment) */ -"6jXf0i" = "圓輪選項"; - -/* (No Comment) */ -"8jLN0l-b7SqPZ" = "確認選「歲月之輪」嗎?"; - -/* (No Comment) */ -"8jLN0l-LF5EdZ" = "確認選「默認」嗎?"; - -/* (No Comment) */ -"8jLN0l-Q39il4" = "確認選「日月光華」嗎?"; - -/* (No Comment) */ -"axjc3N" = "月出入"; - -/* (No Comment) */ -"b7SqPZ" = "歲月之輪"; - -/* (No Comment) */ -"CX2NbA-b7SqPZ" = "共有${count}種「歲月之輪」。"; - -/* (No Comment) */ -"CX2NbA-LF5EdZ" = "共有${count}種「默認」。"; - -/* (No Comment) */ -"CX2NbA-Q39il4" = "共有${count}種「日月光華」。"; - -/* (No Comment) */ -"gpCwrM" = "純文字"; - -/* (No Comment) */ -"j6yiGJ" = "默認"; - -/* (No Comment) */ -"LF5EdZ" = "默認"; - -/* (No Comment) */ -"NjcTZd" = "日出入"; - -/* (No Comment) */ -"nsPERY" = "圓輪選項"; - -/* (No Comment) */ -"oDF3vf" = "時計"; - -/* (No Comment) */ -"pIy9tn-4GqETF" = "共有${count}種「月相」。"; - -/* (No Comment) */ -"pIy9tn-axjc3N" = "共有${count}種「月出入」。"; - -/* (No Comment) */ -"pIy9tn-j6yiGJ" = "共有${count}種「默認」。"; - -/* (No Comment) */ -"pIy9tn-NjcTZd" = "共有${count}種「日出入」。"; - -/* (No Comment) */ -"pIy9tn-rNmT7n" = "共有${count}種「節氣」。"; - -/* (No Comment) */ -"Q39il4" = "日月光華"; - -/* (No Comment) */ -"rNmT7n" = "節氣"; - -/* (No Comment) */ -"rTHEbQ-4GqETF" = "確認選「月相」嗎?"; - -/* (No Comment) */ -"rTHEbQ-axjc3N" = "確認選「月出入」嗎?"; - -/* (No Comment) */ -"rTHEbQ-j6yiGJ" = "確認選「默認」嗎?"; - -/* (No Comment) */ -"rTHEbQ-NjcTZd" = "確認選「日出入」嗎?"; - -/* (No Comment) */ -"rTHEbQ-rNmT7n" = "確認選「節氣」嗎?"; - -/* (No Comment) */ -"tVvJ9c" = "純文字掛件選項"; - -/* (No Comment) */ -"uKd3Ry" = "圓輪掛件選項"; - -/* (No Comment) */ -"Ymgolg" = "圓輪"; - -/* (No Comment) */ -"ZAfzGO" = "時計選項"; - diff --git a/Widget/Base.lproj/Widget.intentdefinition b/Widget/Base.lproj/Widget.intentdefinition deleted file mode 100644 index 5a10c38..0000000 --- a/Widget/Base.lproj/Widget.intentdefinition +++ /dev/null @@ -1,586 +0,0 @@ - - - - - INEnums - - - INEnumDisplayName - Display Mode - INEnumDisplayNameID - XwYYHO - INEnumGeneratesHeader - - INEnumName - DisplayMode - INEnumType - Regular - INEnumValues - - - INEnumValueDisplayName - Default - INEnumValueDisplayNameID - F0IoDR - INEnumValueName - unknown - - - INEnumValueDisplayName - Date - INEnumValueDisplayNameID - qgxKar - INEnumValueIndex - 1 - INEnumValueName - date - - - INEnumValueDisplayName - Time - INEnumValueDisplayNameID - oeabNo - INEnumValueIndex - 2 - INEnumValueName - time - - - - - INEnumDisplayName - Display Order - INEnumDisplayNameID - LzSe8F - INEnumGeneratesHeader - - INEnumName - DisplayOrder - INEnumType - Regular - INEnumValues - - - INEnumValueDisplayName - default - INEnumValueDisplayNameID - XhZUWJ - INEnumValueName - unknown - - - INEnumValueDisplayName - Date Left - INEnumValueDisplayNameID - AofoHj - INEnumValueIndex - 1 - INEnumValueName - dateLeft - - - INEnumValueDisplayName - Date Right - INEnumValueDisplayNameID - jNk9HH - INEnumValueIndex - 2 - INEnumValueName - dateRight - - - - - INIntentDefinitionModelVersion - 1.2 - INIntentDefinitionNamespace - 88xZPY - INIntentDefinitionSystemVersion - 22E261 - INIntentDefinitionToolsBuildVersion - 14E222b - INIntentDefinitionToolsVersion - 14.3 - INIntents - - - INIntentCategory - information - INIntentClassName - SmallIntent - INIntentDescription - Small widget options - INIntentDescriptionID - tVvJ9c - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 4 - INIntentName - Small - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Display Mode - INIntentParameterDisplayNameID - gQ74GO - INIntentParameterDisplayPriority - 1 - INIntentParameterEnumType - DisplayMode - INIntentParameterEnumTypeNamespace - 88xZPY - INIntentParameterName - mode - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${mode}’. - INIntentParameterPromptDialogFormatStringID - A2Gu23 - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${mode}’? - INIntentParameterPromptDialogFormatStringID - rnexwt - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterTag - 2 - INIntentParameterType - Integer - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Back Alpha - INIntentParameterDisplayNameID - 02xN5Y - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataMaximumValue - 0.5 - INIntentParameterMetadataType - Slider - - INIntentParameterName - backAlpha - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Set a value for background transparancy - INIntentParameterPromptDialogFormatStringID - vzqK2A - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsResolution - - INIntentParameterTag - 4 - INIntentParameterType - Decimal - INIntentParameterUnsupportedReasons - - - INIntentParameterUnsupportedReasonCode - negativeNumbersNotSupported - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be negative. - INIntentParameterUnsupportedReasonFormatStringID - tuT0Ze - - - INIntentParameterUnsupportedReasonCode - greaterThanMaximumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be higher than ${maximumValue}. - INIntentParameterUnsupportedReasonFormatStringID - EUjqke - - - INIntentParameterUnsupportedReasonCode - lessThanMinimumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be lower than ${minimumValue}. - INIntentParameterUnsupportedReasonFormatStringID - 4MUCeV - - - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Small - INIntentTitleID - gpCwrM - INIntentType - Custom - INIntentVerb - View - - - INIntentCategory - information - INIntentClassName - MediumIntent - INIntentDescription - Medium widget options - INIntentDescriptionID - vyyVxE - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 4 - INIntentName - Medium - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Display Order - INIntentParameterDisplayNameID - xetndN - INIntentParameterDisplayPriority - 1 - INIntentParameterEnumType - DisplayOrder - INIntentParameterEnumTypeNamespace - 88xZPY - INIntentParameterName - order - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Primary - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${order}’. - INIntentParameterPromptDialogFormatStringID - v50Klc - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${order}’? - INIntentParameterPromptDialogFormatStringID - FJbM3s - INIntentParameterPromptDialogType - Confirmation - - - INIntentParameterTag - 2 - INIntentParameterType - Integer - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Back Alpha - INIntentParameterDisplayNameID - vouhqV - INIntentParameterDisplayPriority - 2 - INIntentParameterMetadata - - INIntentParameterMetadataMaximumValue - 0.5 - INIntentParameterMetadataType - Slider - - INIntentParameterName - backAlpha - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Set a value for background transparancy - INIntentParameterPromptDialogFormatStringID - oMSedJ - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsResolution - - INIntentParameterTag - 4 - INIntentParameterType - Decimal - INIntentParameterUnsupportedReasons - - - INIntentParameterUnsupportedReasonCode - negativeNumbersNotSupported - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be negative. - INIntentParameterUnsupportedReasonFormatStringID - xJGaGp - - - INIntentParameterUnsupportedReasonCode - greaterThanMaximumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be higher than ${maximumValue}. - INIntentParameterUnsupportedReasonFormatStringID - jhRO44 - - - INIntentParameterUnsupportedReasonCode - lessThanMinimumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be lower than ${minimumValue}. - INIntentParameterUnsupportedReasonFormatStringID - O25A4M - - - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Medium - INIntentTitleID - GeXaBx - INIntentType - Custom - INIntentVerb - View - - - INIntentCategory - information - INIntentClassName - LargeIntent - INIntentDescription - Large widget options - INIntentDescriptionID - FP4Udk - INIntentEligibleForWidgets - - INIntentIneligibleForSuggestions - - INIntentLastParameterTag - 2 - INIntentName - Large - INIntentParameters - - - INIntentParameterConfigurable - - INIntentParameterDisplayName - Back Alpha - INIntentParameterDisplayNameID - QaJIBA - INIntentParameterDisplayPriority - 1 - INIntentParameterMetadata - - INIntentParameterMetadataMaximumValue - 0.5 - INIntentParameterMetadataType - Slider - - INIntentParameterName - backAlpha - INIntentParameterPromptDialogs - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogType - Configuration - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Set a value for background transparancy - INIntentParameterPromptDialogFormatStringID - QiDXla - INIntentParameterPromptDialogType - Primary - - - INIntentParameterSupportsResolution - - INIntentParameterTag - 2 - INIntentParameterType - Decimal - INIntentParameterUnsupportedReasons - - - INIntentParameterUnsupportedReasonCode - negativeNumbersNotSupported - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be negative. - INIntentParameterUnsupportedReasonFormatStringID - v0khyr - - - INIntentParameterUnsupportedReasonCode - greaterThanMaximumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be higher than ${maximumValue}. - INIntentParameterUnsupportedReasonFormatStringID - uq6y5D - - - INIntentParameterUnsupportedReasonCode - lessThanMinimumValue - INIntentParameterUnsupportedReasonDefault - - INIntentParameterUnsupportedReasonFormatString - ${displayName} can’t be lower than ${minimumValue}. - INIntentParameterUnsupportedReasonFormatStringID - BW541T - - - - - INIntentResponse - - INIntentResponseCodes - - - INIntentResponseCodeName - success - INIntentResponseCodeSuccess - - - - INIntentResponseCodeName - failure - - - - INIntentTitle - Large - INIntentTitleID - Uh8qA0 - INIntentType - Custom - INIntentVerb - View - - - INTypes - - - diff --git a/Widget/Dual.swift b/Widget/Dual.swift new file mode 100644 index 0000000..d0d6eb8 --- /dev/null +++ b/Widget/Dual.swift @@ -0,0 +1,152 @@ +// +// Dual.swift +// Chinese Time +// +// Created by Leo Liu on 6/28/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +enum DisplayOrder: String, AppEnum { + case dateFirst, timeFirst + + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之順序") + static var caseDisplayRepresentations: [DisplayOrder : DisplayRepresentation] = [ + .dateFirst: .init(title: "日左時右"), + .timeFirst: .init(title: "時左日右"), + ] +} + +struct MediumConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "MediumIntent" + static var title: LocalizedStringResource = "雙錶" + static var description = IntentDescription("雙錶以同時展現日時,順序可選") + + @Parameter(title: "順序", default: .dateFirst) + var order: DisplayOrder + + @Parameter(title: "背景灰度", default: 0, controlStyle: .slider, inclusiveRange: (0, 1)) + var backAlpha: Double + + static var parameterSummary: some ParameterSummary { + Summary { + \.$order + \.$backAlpha + } + } +} + +struct MediumProvider: AppIntentTimelineProvider { + 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 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) + } +} + +struct MediumEntry: TimelineEntry, ChineseTimeEntry { + let date: Date + let configuration: MediumProvider.Intent + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let relevance: TimelineEntryRelevance? + + init(configuration: MediumProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + date = chineseCalendar.time + self.configuration = configuration + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: chineseCalendar.nextQuarters(count: 1)[0])) + } +} + +struct MediumWidgetEntryView: View { + var entry: MediumProvider.Entry + @Environment(\.widgetFamily) var widgetFamily + + init(entry: MediumProvider.Entry) { + self.entry = entry + } + + func backColor() -> Color { + return Color.gray.opacity(entry.configuration.backAlpha) + } + + var body: some View { + let isLarge = widgetFamily == .systemExtraLarge + + GeometryReader { proxy in + 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) + 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) + } + } + .padding(.horizontal, (proxy.size.width - proxy.size.height * 2) * 0.25) + } + .containerBackground(backColor(), for: .widget) + .padding(5) + } +} + + +struct MediumWidget: Widget { + static let kind: String = "Medium" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: MediumProvider.Intent.self, provider: MediumProvider()) { entry in + MediumWidgetEntryView(entry: entry) + } + .contentMarginsDisabled() + .containerBackgroundRemovable() + .configurationDisplayName("雙錶") + .description("雙錶以同時展現日時") + .supportedFamilies([.systemMedium, .systemExtraLarge]) + } +} + +#Preview("Medium", as: .systemMedium, using: { + let intent = MediumProvider.Intent() + intent.order = .dateFirst + intent.backAlpha = 0.2 + return intent +}(), widget: { + MediumWidget() +}, timelineProvider: { + MediumProvider() +}) diff --git a/Widget/Full.swift b/Widget/Full.swift new file mode 100644 index 0000000..45d2853 --- /dev/null +++ b/Widget/Full.swift @@ -0,0 +1,123 @@ +// +// Widget.swift +// iOSWidgetExtension +// +// Created by Leo Liu on 5/9/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +struct LargeConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "LargeIntent" + static var title: LocalizedStringResource = "全錶" + static var description = IntentDescription("完整錶面") + + @Parameter(title: "背景灰度", default: 0, controlStyle: .slider, inclusiveRange: (0, 1)) + var backAlpha: Double + + static var parameterSummary: some ParameterSummary { + Summary { + \.$backAlpha + } + } +} + +struct LargeProvider: AppIntentTimelineProvider { + 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 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) + } +} + +struct LargeEntry: TimelineEntry, ChineseTimeEntry { + let date: Date + let configuration: LargeProvider.Intent + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let relevance: TimelineEntryRelevance? + + init(configuration: LargeProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + date = chineseCalendar.time + self.configuration = configuration + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: chineseCalendar.nextQuarters(count: 1)[0])) + } +} + +struct LargeWidgetEntryView: View { + @Environment(\.widgetFamily) var widgetFamily + var entry: LargeProvider.Entry + + init(entry: LargeProvider.Entry) { + self.entry = entry + } + + func backColor() -> Color { + return Color.gray.opacity(entry.configuration.backAlpha) + } + + 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) + .containerBackground(backColor(), for: .widget) + .padding(5) + } +} + +struct LargeWidget: Widget { + static let kind: String = "Large" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: LargeProvider.Intent.self, provider: LargeProvider()) { entry in + LargeWidgetEntryView(entry: entry) + } + .contentMarginsDisabled() + .containerBackgroundRemovable() + .configurationDisplayName("全錶") + .description("完整華曆錶") + .supportedFamilies([.systemSmall, .systemLarge]) + } +} + +#Preview("Large", as: .systemLarge, using: { + let intent = LargeProvider.Intent() + intent.backAlpha = 0.2 + return intent +}(), widget: { + LargeWidget() +}, timelineProvider: { + LargeProvider() +}) diff --git a/Widget/Localizable.xcstrings b/Widget/Localizable.xcstrings new file mode 100644 index 0000000..30c93d4 --- /dev/null +++ b/Widget/Localizable.xcstrings @@ -0,0 +1,896 @@ +{ + "sourceLanguage" : "zh-Hant", + "strings" : { + "Default" : { + "comment" : "Default save file name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "常备" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "常備" + } + } + } + }, + "全錶" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Full Watch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全表" + } + } + } + }, + "列於四隅之時計" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown as corner widgets" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列于四隅之时计" + } + } + } + }, + "圓輪" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circular" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "圆轮" + } + } + } + }, + "圓輪掛件選項" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circular widget config" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "圆轮挂件选项" + } + } + } + }, + "型制" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Type" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "型制" + } + } + } + }, + "太陰" : { + "comment" : "Moon can not be located", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "月" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "月" + } + } + } + }, + "太陽" : { + "comment" : "Sun can not be located", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sun" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + } + } + }, + "完整華曆錶" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Complete Chinese Time watch face" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整华历表" + } + } + } + }, + "完整錶面" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Full Watch Face" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整表面" + } + } + } + }, + "寫有華曆日時之片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Card with Chinese Time in gradient text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "写有华历日时之片" + } + } + } + }, + "展現日時之圓輪" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ring showing time or date" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展现日时之圆轮" + } + } + } + }, + "文字" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字" + } + } + } + }, + "日" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + } + } + }, + "日出入" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunrise & Sunset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日出入" + } + } + } + }, + "日左時右" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date & Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日左时右" + } + } + } + }, + "日時之擇一" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose between date and time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日时之择一" + } + } + } + }, + "日時之順序" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display order of date and time components" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日时之顺序" + } + } + } + }, + "日月光華" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sun & Moon Times" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日月光华" + } + } + } + }, + "日躔" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sun Times" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日躔" + } + } + } + }, + "時" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时" + } + } + } + }, + "時左日右" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Time & Date" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时左日右" + } + } + } + }, + "時計" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时计" + } + } + } + }, + "時計掛件選項" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown widget config" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时计挂件选项" + } + } + } + }, + "時計片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown Card" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时计片" + } + } + } + }, + "時計隅" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown Corner" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时计隅" + } + } + } + }, + "月出入" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moonrise & Moonset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "月出入" + } + } + } + }, + "月相" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moon Phase" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "月相" + } + } + } + }, + "月離" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moon Times" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "月离" + } + } + } + }, + "樸素寫就之華曆" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time in plain text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "朴素写就之华历" + } + } + } + }, + "次月相" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Moon Phase" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "次月相" + } + } + } + }, + "次節氣" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Solar Term" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "次节气" + } + } + } + }, + "歲月之輪" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Month and Day Rings" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "岁月之轮" + } + } + } + }, + "永夜" : { + "comment" : "Sun never rise", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Polar Night" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永夜" + } + } + } + }, + "永恆無盡" : { + "comment" : "Unknown time", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eternity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永恒无尽" + } + } + } + }, + "永日" : { + "comment" : "Sun never set", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Midnight Sun" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永日" + } + } + } + }, + "永月" : { + "comment" : "Moon never set", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Long Moon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永月" + } + } + } + }, + "永無月" : { + "comment" : "Moon never rise", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Moon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永无月" + } + } + } + }, + "目的" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Target" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "目的" + } + } + } + }, + "節氣" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solar Term" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "节气" + } + } + } + }, + "簡化之輪以展現日時" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concise rings date or time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简化之轮以展现日时" + } + } + } + }, + "簡化之錶以展現日時之一" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concise watch face to focus on date or time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简化之表以展现日时之一" + } + } + } + }, + "簡單華曆文字" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time in plain text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简单华历文字" + } + } + } + }, + "簡錶" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concise Watch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简表" + } + } + } + }, + "背景灰度" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Back Greyness" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "背景灰度" + } + } + } + }, + "華曆" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历" + } + } + } + }, + "華曆文字" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time in Text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历文字" + } + } + } + }, + "華曆片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chinese Time on Card" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "华历片" + } + } + } + }, + "距離次事件之倒計時" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown to next event" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "距离次事件之倒计时" + } + } + } + }, + "距離次事件之倒計時片" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Countdown card for next event" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "距离次事件之倒计时片" + } + } + } + }, + "雙錶" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dual Watch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "双表" + } + } + } + }, + "雙錶以同時展現日時" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dual watch face for separate display of date and time" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "双表以同时展现日时" + } + } + } + }, + "雙錶以同時展現日時,順序可選" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dual watch face for separate display of date and time, in the order of your choice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "双表以同时展现日时,顺序可选" + } + } + } + }, + "順序" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Order" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "顺序" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Widget/Single.swift b/Widget/Single.swift new file mode 100644 index 0000000..b718e22 --- /dev/null +++ b/Widget/Single.swift @@ -0,0 +1,161 @@ +// +// Single.swift +// Chinese Time +// +// Created by Leo Liu on 6/28/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +enum DisplayMode: String, AppEnum { + case date, time + + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之擇一") + static var caseDisplayRepresentations: [DisplayMode : DisplayRepresentation] = [ + .date: .init(title: "日"), + .time: .init(title: "時"), + ] +} + +struct SmallConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "SmallIntent" + static var title: LocalizedStringResource = "簡錶" + static var description = IntentDescription("簡化之錶以展現日時之一") + + @Parameter(title: "型制", default: .time) + var mode: DisplayMode + + @Parameter(title: "背景灰度", default: 0, controlStyle: .slider, inclusiveRange: (0, 1)) + var backAlpha: Double + + static var parameterSummary: some ParameterSummary { + Summary { + \.$mode + \.$backAlpha + } + } +} + +struct SmallProvider: AppIntentTimelineProvider { + 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 { + 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 { + let date: Date + let configuration: SmallProvider.Intent + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let relevance: TimelineEntryRelevance? + + init(configuration: SmallProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + date = chineseCalendar.time + self.configuration = configuration + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + switch configuration.mode { + case .time: + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: chineseCalendar.nextQuarters(count: 1)[0])) + case .date: + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: chineseCalendar.nextHours(count: 1)[0])) + } + } +} + +struct SmallWidgetEntryView: View { + var entry: SmallProvider.Entry + var backColor: Color { + Color.gray.opacity(entry.configuration.backAlpha) + } + + 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) + .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) + .containerBackground(backColor, for: .widget) + .padding(5) + } + } +} + +struct SmallWidget: Widget { + static let kind: String = "Small" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: SmallProvider.Intent.self, provider: SmallProvider()) { entry in + SmallWidgetEntryView(entry: entry) + } + .contentMarginsDisabled() + .containerBackgroundRemovable() + .configurationDisplayName("簡錶") + .description("簡化之錶以展現日時之一") + .supportedFamilies([.systemSmall]) + } +} + + +#Preview("Small Date", as: .systemSmall, using: { + let intent = SmallProvider.Intent() + intent.mode = .date + intent.backAlpha = 0.2 + return intent +}(), widget: { + SmallWidget() +}, timelineProvider: { + SmallProvider() +}) + +#Preview("Small Time", as: .systemSmall, using: { + let intent = SmallProvider.Intent() + intent.mode = .time + intent.backAlpha = 0.2 + return intent +}(), widget: { + SmallWidget() +}, timelineProvider: { + SmallProvider() +}) diff --git a/Widget/TaskGroup.swift b/Widget/TaskGroup.swift new file mode 100644 index 0000000..d4c253a --- /dev/null +++ b/Widget/TaskGroup.swift @@ -0,0 +1,32 @@ +// +// TaskGroup.swift +// Chinese Time Mac +// +// Created by Leo Liu on 9/3/23. +// + +import WidgetKit +import AppIntents + +protocol ChineseTimeEntry { + associatedtype Intent: WidgetConfigurationIntent + init(configuration: Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) +} + +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 { + group.addTask { + return Entry(configuration: configuration, chineseCalendar: calendar, watchLayout: watchLayout) + } + } + for await result in group { + entries.append(result) + } + } + entries.sort { $0.date < $1.date } + return entries +} + + diff --git a/Widget/WatchWidgets/Circular.swift b/Widget/WatchWidgets/Circular.swift new file mode 100644 index 0000000..826196a --- /dev/null +++ b/Widget/WatchWidgets/Circular.swift @@ -0,0 +1,247 @@ +// +// Circular.swift +// Chinese Time +// +// Created by Leo Liu on 6/28/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +enum CircularMode: String, AppEnum { + case daylight, monthDay + + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "圓輪掛件選項") + static var caseDisplayRepresentations: [CircularMode : DisplayRepresentation] = [ + .daylight: .init(title: "日月光華"), + .monthDay: .init(title: "歲月之輪"), + ] +} + +struct CircularConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "CircularIntent" + static var title: LocalizedStringResource = "圓輪" + static var description = IntentDescription("簡化之輪以展現日時") + + @Parameter(title: "型制", default: .daylight) + var mode: CircularMode + + static var parameterSummary: some ParameterSummary { + Summary { + \.$mode + } + } +} + +struct CircularProvider: AppIntentTimelineProvider { + 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 { + 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] { + let daylight = Intent() + daylight.mode = .daylight + let monthDay = Intent() + monthDay.mode = .monthDay + return [ + AppIntentRecommendation(intent: daylight, description: "日月光華"), + AppIntentRecommendation(intent: monthDay, description: "歲月之輪") + ] + } +} + +private func sunTimes(times: [ChineseCalendar.NamedPosition?]) -> (start: CGFloat, end: CGFloat)? { + guard times.count == 5 else { return nil } + if let sunrise = times[1]?.pos, let sunset = times[3]?.pos { + return (start: CGFloat(sunrise), end: CGFloat(sunset)) + } else if times[1] == nil && times[3] == nil { + if let _ = times[2]?.pos { + return (start: 0, end: 1) + } else { + return (start: 0, end: 1e-7) + } + } else { + if let sunrise = times[1]?.pos { + return (start: CGFloat(sunrise), end: 1.0) + } else if let sunset = times[3]?.pos { + return (start: 0, end: CGFloat(sunset)) + } else { + return (start: 0, end: 1e-7) + } + } +} + +private func moonTimes(times: [ChineseCalendar.NamedPosition?]) -> ((start: CGFloat, end: CGFloat)?, CGFloat?) { + guard times.count == 6 else { return (nil, nil) } + if let firstMoonRise = times[0]?.pos { + if let firstMoonSet = times[2]?.pos { + return ((start: CGFloat(firstMoonRise), end: CGFloat(firstMoonSet)), times[1].flatMap { CGFloat($0.pos) }) + } else { + return ((start: CGFloat(firstMoonRise), end: 1.0), times[1].flatMap { CGFloat($0.pos) } ?? 1.0) + } + } else if let secondMoonSet = times[5]?.pos { + if let secondMoonRise = times[3]?.pos { + return ((start: CGFloat(secondMoonRise), end: CGFloat(secondMoonSet)), times[4].flatMap { CGFloat($0.pos) }) + } else { + return ((start: 0.0, end: CGFloat(secondMoonSet)), times[4].flatMap { CGFloat($0.pos) } ?? 0.0) + } + } else if let firstMoonSet = times[2]?.pos, let secondMoonRise = times[3]?.pos { + return ((start: CGFloat(secondMoonRise), end: CGFloat(firstMoonSet)), (times[1] ?? times[4]).flatMap { CGFloat($0.pos) }) + } else { + if let firstMoonSet = times[2]?.pos { + return ((start: 0, end: CGFloat(firstMoonSet)), times[1].flatMap { CGFloat($0.pos) } ?? 0.0) + } else if let secondMoonRise = times[3]?.pos { + return ((start: CGFloat(secondMoonRise), end: 1.0), times[4].flatMap { CGFloat($0.pos) } ?? 1.0) + } else { + if times[1] != nil || times[4] != nil { + return ((start: 0, end: 1), nil) + } else { + return ((start: 0, end: 1e-7), nil) + } + } + } +} + +struct CircularEntry: TimelineEntry, ChineseTimeEntry { + let date: Date + let configuration: CircularProvider.Intent + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let inner: (CGFloat, CGFloat) + let outer: (CGFloat, CGFloat) + let innerGradient: Gradient + let outerGradient: Gradient + let current: CGFloat? + let innerDirection: CGFloat? + let currentColor: Color? + let relevance: TimelineEntryRelevance? + + init(configuration: CircularProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + date = chineseCalendar.time + self.configuration = configuration + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + let phase = StartingPhase() + + switch configuration.mode { + case .monthDay: + outer = (start: phase.firstRing, end: chineseCalendar.currentDayInYear + phase.firstRing) + inner = (start: phase.secondRing, end: chineseCalendar.currentDayInMonth + phase.secondRing) + outerGradient = applyGradient(gradient: watchLayout.firstRing, startingAngle: phase.firstRing) + innerGradient = applyGradient(gradient: watchLayout.secondRing, startingAngle: phase.secondRing) + current = nil + innerDirection = nil + currentColor = nil + relevance = TimelineEntryRelevance(score: 5, duration: 3600) + + case .daylight: + let (inner, innerDirection) = moonTimes(times: chineseCalendar.sunMoonPositions.lunar) + let outer = sunTimes(times: chineseCalendar.sunMoonPositions.solar) + self.inner = inner ?? (start: 0, end: 1e-7) + self.innerDirection = innerDirection + self.outer = outer ?? (start: 0, end: 1e-7) + outerGradient = applyGradient(gradient: watchLayout.thirdRing, startingAngle: phase.thirdRing) + innerGradient = applyGradient(gradient: watchLayout.secondRing, startingAngle: phase.secondRing) + current = chineseCalendar.currentHourInDay + currentColor = Color(cgColor: watchLayout.thirdRing.interpolate(at: chineseCalendar.currentHourInDay)) + relevance = TimelineEntryRelevance(score: 5, duration: 864) + } + } +} + +struct CircularEntryView: View { + var entry: CircularProvider.Entry + + var body: some View { + switch entry.configuration.mode { + case .monthDay: + Circular(outer: entry.outer, inner: entry.inner, outerGradient: entry.outerGradient, innerGradient: entry.innerGradient) + .containerBackground(Color.clear, for: .widget) + .widgetLabel { + Text(String(entry.chineseCalendar.dateString.reversed())) + } + case .daylight: + Circular(outer: entry.outer, inner: entry.inner, current: entry.current, innerDirection: entry.innerDirection, outerGradient: entry.outerGradient, innerGradient: entry.innerGradient, currentColor: entry.currentColor) + .containerBackground(Color.clear, for: .widget) + .widgetLabel { + Text(String((entry.chineseCalendar.hourString + entry.chineseCalendar.shortQuarterString).reversed())) + } + } + } +} + +struct CircularWidget: Widget { + static let kind: String = "Circular" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: CircularProvider.Intent.self, provider: CircularProvider()) { entry in + CircularEntryView(entry: entry) + } + .containerBackgroundRemovable() + .configurationDisplayName("圓輪") + .description("展現日時之圓輪") +#if os(watchOS) + .supportedFamilies([.accessoryCircular, .accessoryCorner]) +#else + .supportedFamilies([.accessoryCircular]) +#endif + } +} + +#Preview("Circular Daylight", as: .accessoryCircular, using: { + let intent = CircularProvider.Intent() + intent.mode = .daylight + return intent +}()) { + CircularWidget() +} timelineProvider: { + CircularProvider() +} + +#Preview("Circular Monthday", as: .accessoryCircular, using: { + let intent = CircularProvider.Intent() + intent.mode = .monthDay + return intent +}()) { + CircularWidget() +} timelineProvider: { + CircularProvider() +} diff --git a/Widget/WatchWidgets/Corner.swift b/Widget/WatchWidgets/Corner.swift new file mode 100644 index 0000000..6abc02b --- /dev/null +++ b/Widget/WatchWidgets/Corner.swift @@ -0,0 +1,63 @@ +// +// Corner.swift +// Watch Widget Extension +// +// Created by Leo Liu on 6/28/23. +// + +import SwiftUI +import WidgetKit + +struct CurveWidget: Widget { + static let kind: String = "Corner" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: CountDownProvider.Intent.self, provider: CountDownProvider()) { entry in + CountDownEntryView(entry: entry) + .containerBackground(Material.thin, for: .widget) + } + .configurationDisplayName("時計隅") + .description("列於四隅之時計") + .supportedFamilies([.accessoryCorner]) + } +} + +#Preview("Sunrise", as: .accessoryCorner, using: { + let intent = CountDownProvider.Intent() + intent.target = .sunriseSet + return intent +}(), widget: { + CurveWidget() +}, timelineProvider: { + CountDownProvider() +}) + +#Preview("Moonrise", as: .accessoryCorner, using: { + let intent = CountDownProvider.Intent() + intent.target = .moonriseSet + return intent +}(), widget: { + CurveWidget() +}, timelineProvider: { + CountDownProvider() +}) + +#Preview("Solar Terms", as: .accessoryCorner, using: { + let intent = CountDownProvider.Intent() + intent.target = .solarTerms + return intent +}(), widget: { + CurveWidget() +}, timelineProvider: { + CountDownProvider() +}) + +#Preview("Moon Phases", as: .accessoryCorner, using: { + let intent = CountDownProvider.Intent() + intent.target = .lunarPhases + return intent +}(), widget: { + CurveWidget() +}, timelineProvider: { + CountDownProvider() +}) diff --git a/Widget/WatchWidgets/CountDown.swift b/Widget/WatchWidgets/CountDown.swift new file mode 100644 index 0000000..10d8cf4 --- /dev/null +++ b/Widget/WatchWidgets/CountDown.swift @@ -0,0 +1,373 @@ +// +// CountDown.swift +// Chinese Time +// +// Created by Leo Liu on 6/28/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +struct CountDownProvider: AppIntentTimelineProvider { + 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 { + case .moonriseSet: + nextMoonTimes(chineseCalendar: chineseCalendar) + case .sunriseSet: + nextSunTimes(chineseCalendar: chineseCalendar) + case .lunarPhases: + nextMoonPhase(chineseCalendar: chineseCalendar) + case .solarTerms: + nextSolarTerm(chineseCalendar: chineseCalendar) + } + let entryDates = 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] { + let solarTerms = { let intent = Intent(); intent.target = .solarTerms; return intent }() + let lunarPhases = { let intent = Intent(); intent.target = .lunarPhases; return intent }() + let sunriseSet = { let intent = Intent(); intent.target = .sunriseSet; return intent }() + let moonriseSet = { let intent = Intent(); intent.target = .moonriseSet; return intent }() + + return [ + AppIntentRecommendation(intent: solarTerms, description: "次節氣"), + AppIntentRecommendation(intent: lunarPhases, description: "次月相"), + AppIntentRecommendation(intent: sunriseSet, description: "日出入"), + AppIntentRecommendation(intent: moonriseSet, description: "月出入") + ] + } +} + +private func find(in dates: [ChineseCalendar.NamedDate], at date: Date) -> (ChineseCalendar.NamedDate?, ChineseCalendar.NamedDate?) { + if dates.count > 1 { + let atDate = ChineseCalendar.NamedDate(name: "", date: date) + let index = dates.insertionIndex(of: atDate, comparison: { $0.date < $1.date }) + if index > 0 && index < dates.count { + let previous = dates[index - 1] + let next = dates[index] + if Date.now.distance(to: date) < 30 || previous.date.distance(to: date) < date.distance(to: next.date) { + return (previous: previous, next: next) + } else { + return (previous: next, next: index+1 < dates.count ? dates[index + 1] : nil) + } + } else if index < dates.count { + return (previous: nil, next: dates[index]) + } else { + return (previous: dates[index - 1], next: nil) + } + } else { + return (previous: nil, next: nil) + } +} + +struct CountDownEntry: TimelineEntry, ChineseTimeEntry { + let date: Date + let configuration: CountDownProvider.Intent + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let previousDate: ChineseCalendar.NamedDate? + let nextDate: ChineseCalendar.NamedDate? + let color: CGColor + let barColor: CGColor + let relevance: TimelineEntryRelevance? + + init(configuration: CountDownProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + self.configuration = configuration + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + switch configuration.target { + case .lunarPhases: + self.date = chineseCalendar.startOfNextDay + (previousDate, nextDate) = find(in: chineseCalendar.moonPhases, at: chineseCalendar.time) + if let next = nextDate { + color = next.name == ChineseCalendar.moonPhases[0] ? watchLayout.eclipseIndicator : watchLayout.fullmoonIndicator + barColor = if next.name == ChineseCalendar.moonPhases[0] { // New moon + watchLayout.secondRing.interpolate(at: chineseCalendar.eventInMonth.eclipse.first?.pos ?? 0.0) + } else { // Full moon + watchLayout.secondRing.interpolate(at: chineseCalendar.eventInMonth.fullMoon.first?.pos ?? 0.5) + } + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: next.date)) + } else { + color = watchLayout.fullmoonIndicator + barColor = CGColor(gray: 0, alpha: 0) + relevance = TimelineEntryRelevance(score: 0) + } + + case .solarTerms: + self.date = chineseCalendar.startOfNextDay + (previousDate, nextDate) = find(in: chineseCalendar.solarTerms, at: chineseCalendar.time) + if let next = nextDate, let _ = previousDate { + color = ChineseCalendar.evenSolarTermChinese.contains(next.name) ? watchLayout.evenStermIndicator : watchLayout.oddStermIndicator + barColor = { + let yearStart = chineseCalendar.solarTerms[0].date + let yearEnd = chineseCalendar.solarTerms[24].date + return watchLayout.firstRing.interpolate(at: yearStart.distance(to: next.date) / yearStart.distance(to: yearEnd)) + }() + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: next.date)) + } else { + color = watchLayout.evenStermIndicator + barColor = CGColor(gray: 0, alpha: 0) + relevance = TimelineEntryRelevance(score: 0) + } + + case .moonriseSet: + self.date = chineseCalendar.time + 1800 // Half Hour + let moonriseAndSet = chineseCalendar.moonTimes.filter { $0.name == ChineseCalendar.moonTimeName[0] || $0.name == ChineseCalendar.moonTimeName[2] } + (previousDate, nextDate) = find(in: moonriseAndSet, at: chineseCalendar.time) + if let next = nextDate { + color = next.name == ChineseCalendar.moonTimeName[0] ? watchLayout.moonPositionIndicator[0] : watchLayout.moonPositionIndicator[2] + barColor = if next.name == ChineseCalendar.moonTimeName[0] { // Moonrise + watchLayout.secondRing.interpolate(at: chineseCalendar.sunMoonPositions.lunar[0]?.pos ?? chineseCalendar.sunMoonPositions.lunar[3]?.pos ?? 0.25) + } else { // Moonset + watchLayout.secondRing.interpolate(at: chineseCalendar.sunMoonPositions.lunar[2]?.pos ?? chineseCalendar.sunMoonPositions.lunar[5]?.pos ?? 0.75) + } + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: next.date)) + } else { + color = watchLayout.moonPositionIndicator[1] + barColor = CGColor(gray: 0, alpha: 0) + relevance = TimelineEntryRelevance(score: 0) + } + + case .sunriseSet: + self.date = chineseCalendar.time + 1800 // Half Hour + let sunriseAndSet = chineseCalendar.sunTimes.filter { $0.name == ChineseCalendar.dayTimeName[1] || $0.name == ChineseCalendar.dayTimeName[3] } + (previousDate, nextDate) = find(in: sunriseAndSet, at: chineseCalendar.time) + if let next = nextDate { + color = next.name == ChineseCalendar.dayTimeName[1] ? watchLayout.sunPositionIndicator[1] : watchLayout.sunPositionIndicator[3] + barColor = if next.name == ChineseCalendar.dayTimeName[1] { // Sunrise + watchLayout.thirdRing.interpolate(at: chineseCalendar.sunMoonPositions.solar[1]?.pos ?? 0.25) + } else { // Sunset + watchLayout.thirdRing.interpolate(at: chineseCalendar.sunMoonPositions.solar[3]?.pos ?? 0.75) + } + relevance = TimelineEntryRelevance(score: 10, duration: date.distance(to: next.date)) + } else { + color = watchLayout.sunPositionIndicator[2] + barColor = CGColor(gray: 0, alpha: 0) + relevance = TimelineEntryRelevance(score: 0) + } + } + } +} + +struct CountDownEntryView: View { + var entry: CountDownEntry + @Environment(\.widgetFamily) var family + + var body: some View { + switch entry.configuration.target { + case .solarTerms: + if let next = entry.nextDate, let previous = entry.previousDate { + let index = findSolarTerm(next.name) + if index >= 0 { + let icon = IconType.solarTerm(view: SolarTerm(angle: CGFloat(index) / 24.0, color: entry.color)) + + switch family { + case .accessoryRectangular: + let name = String(next.name.replacingOccurrences(of: " ", with: "")) + RectanglePanel(icon: icon, name: Text(Locale.isChinese ? name : Locale.translation[name]!), color: entry.color, barColor: entry.barColor, start: previous.date, end: next.date) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, start: previous.date, end: next.date) + default: + EmptyView() + } + } + } + case .lunarPhases: + if let next = entry.nextDate, let previous = entry.previousDate { + let icon = IconType.moon(view: MoonPhase(angle: next.name == ChineseCalendar.moonPhases[0] ? 0.05 : 0.5, color: entry.color)) + + switch family { + case .accessoryRectangular: + RectanglePanel(icon: icon, name: Text(Locale.isChinese ? next.name : Locale.translation[next.name]!), color: entry.color, barColor: entry.barColor, start: previous.date, end: next.date) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, + start: previous.date, end: next.date) + default: + EmptyView() + } + } + + case .sunriseSet: + let noonTimes = entry.chineseCalendar.sunTimes.filter { $0.name == ChineseCalendar.dayTimeName[2]} + if let next = entry.nextDate, let previous = entry.previousDate { + let icon = IconType.sunrise(view: Sun(color: entry.color, rise: next.name == ChineseCalendar.dayTimeName[1])) + + switch family { + case .accessoryRectangular: + RectanglePanel(icon: icon, name: Text(Locale.isChinese ? next.name : Locale.translation[next.name]!), color: entry.color, barColor: entry.barColor, start: previous.date, end: next.date) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, start: previous.date, end: next.date) + default: + EmptyView() + } + + } else { + let icon = IconType.sunrise(view: Sun(color: entry.color, rise: nil)) + + switch family { + case .accessoryRectangular: + let sunDescription = if entry.chineseCalendar.location != nil { + if noonTimes.count > 0 { + Text("永日", comment: "Sun never set") + } else { + Text("永夜", comment: "Sun never rise") + } + } else { + Text("太陽", comment: "Sun can not be located") + } + RectanglePanel(icon: icon, name: sunDescription, color: entry.color, barColor: entry.barColor) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, start: nil, end: nil) + default: + EmptyView() + } + } + case .moonriseSet: + let moonNoonTimes = entry.chineseCalendar.moonTimes.filter { $0.name == ChineseCalendar.moonTimeName[1]} + if let next = entry.nextDate, let previous = entry.previousDate { + let icon = IconType.moon(view: MoonPhase(angle: entry.chineseCalendar.currentDayInMonth, color: entry.color, rise: next.name == ChineseCalendar.moonTimeName[0])) + + switch family { + case .accessoryRectangular: + RectanglePanel(icon: icon, name: Text(Locale.isChinese ? next.name : Locale.translation[next.name]!), color: entry.color, barColor: entry.barColor, start: previous.date, end: next.date) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, + start: previous.date, end: next.date) + default: + EmptyView() + } + } else { + let icon = IconType.moon(view: MoonPhase(angle: entry.chineseCalendar.currentDayInMonth, color: entry.color, rise: nil)) + + switch family { + case .accessoryRectangular: + let moonDescription = if entry.chineseCalendar.location != nil { + if moonNoonTimes.count > 0 { + Text("永月", comment: "Moon never set") + } else { + Text("永無月", comment: "Moon never rise") + } + } else { + Text("太陰", comment: "Moon can not be located") + } + RectanglePanel(icon: icon, name: moonDescription, color: entry.color, barColor: entry.barColor) + case .accessoryCorner: + Curve(icon: icon, barColor: entry.barColor, + start: nil, end: nil) + default: + EmptyView() + } + } + } + } + + private func findSolarTerm(_ solarTerm: String) -> Int { + if let even = ChineseCalendar.evenSolarTermChinese.firstIndex(of: solarTerm) { + return even * 2 + } else if let odd = ChineseCalendar.oddSolarTermChinese.firstIndex(of: solarTerm) { + return odd * 2 + 1 + } else { + return -1 + } + } +} + +struct RectWidget: Widget { + static let kind: String = rectWidgetKind + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: CountDownProvider.Intent.self, provider: CountDownProvider()) { entry in + CountDownEntryView(entry: entry) + .containerBackground(Material.ultraThin, for: .widget) + } + .containerBackgroundRemovable() + .configurationDisplayName("時計片") + .description("距離次事件之倒計時片") + .supportedFamilies([.accessoryRectangular]) + } +} + +#Preview("Lunar Phase", as: .accessoryRectangular, using: { + let intent = CountDownProvider.Intent() + intent.target = .lunarPhases + return intent +}()) { + RectWidget() +} timelineProvider: { + CountDownProvider() +} + +#Preview("Solar Term", as: .accessoryRectangular, using: { + let intent = CountDownProvider.Intent() + intent.target = .solarTerms + return intent +}()) { + RectWidget() +} timelineProvider: { + CountDownProvider() +} + +#Preview("Sunrise", as: .accessoryRectangular, using: { + let intent = CountDownProvider.Intent() + intent.target = .sunriseSet + return intent +}()) { + RectWidget() +} timelineProvider: { + CountDownProvider() +} + +#Preview("Moonrise", as: .accessoryRectangular, using: { + let intent = CountDownProvider.Intent() + intent.target = .moonriseSet + return intent +}()) { + RectWidget() +} timelineProvider: { + CountDownProvider() +} diff --git a/WatchWidget/IconView.swift b/Widget/WatchWidgets/IconView.swift similarity index 95% rename from WatchWidget/IconView.swift rename to Widget/WatchWidgets/IconView.swift index f542e39..17ebbed 100644 --- a/WatchWidget/IconView.swift +++ b/Widget/WatchWidgets/IconView.swift @@ -7,6 +7,12 @@ import SwiftUI +enum IconType { + case solarTerm(view: SolarTerm) + case moon(view: MoonPhase) + case sunrise(view: Sun) +} + struct SolarTerm: View { var angle: CGFloat var color: CGColor @@ -117,7 +123,7 @@ struct MoonPhase: View { moonContext.clip(to: Path(groundMaskPath), options: .inverse) moonContext.translateBy(x: 0, y: minEdge * 0.10) } - var backContext = moonContext + let backContext = moonContext let width = centerRadius * (1.0 + cos(4.0 * CGFloat.pi * angle)) / 2.0 let ellipse = CGMutablePath(ellipseIn: CGRect(x: center.x - width, y: center.y - centerRadius, width: width * 2, height: centerRadius * 2), transform: nil) @@ -224,3 +230,19 @@ struct Sun: View { } } } + +#Preview("SolarTerm") { + SolarTerm(angle: 0.4, color: Color.white.cgColor!) +} + +#Preview("MoonPhase") { + MoonPhase(angle: 0.4, color: CGColor(red: 0.9, green: 0.6, blue: 0.3, alpha: 1)) +} + +#Preview("Moonrise") { + MoonPhase(angle: 0.4, color: CGColor(red: 0.9, green: 0.6, blue: 0.3, alpha: 1), rise: true) +} + +#Preview("Sunrise") { + Sun(color: CGColor(red: 0.9, green: 0.2, blue: 0.1, alpha: 1), rise: true) +} diff --git a/Widget/WatchWidgets/Relevance.swift b/Widget/WatchWidgets/Relevance.swift new file mode 100644 index 0000000..19a907e --- /dev/null +++ b/Widget/WatchWidgets/Relevance.swift @@ -0,0 +1,124 @@ +// +// Events.swift +// Chinese Time +// +// Created by Leo Liu on 6/28/23. +// + +import AppIntents + +enum EventType: String, AppEnum { + case solarTerms, lunarPhases, sunriseSet, moonriseSet + + static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "時計掛件選項") + static var caseDisplayRepresentations: [EventType : DisplayRepresentation] = [ + .solarTerms: .init(title: "節氣"), + .lunarPhases: .init(title: "月相"), + .sunriseSet: .init(title: "日躔"), + .moonriseSet: .init(title: "月離"), + ] +} + +struct CountDownConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "CurveIntent" + static var title: LocalizedStringResource = "時計" + static var description = IntentDescription("距離次事件之倒計時") + + @Parameter(title: "目的", default: .solarTerms) + var target: EventType + + static var parameterSummary: some ParameterSummary { + Summary { + \.$target + } + } +} + +let rectWidgetKind = "Card" + +func updateCountDownRelevantIntents(chineseCalendar: ChineseCalendar) async { + async let sunTimes = nextSunTimes(chineseCalendar: chineseCalendar) + async let moonTimes = nextMoonTimes(chineseCalendar: chineseCalendar) + async let solarTerms = nextSolarTerm(chineseCalendar: chineseCalendar) + async let moonPhases = nextMoonPhase(chineseCalendar: chineseCalendar) + + var relevantIntents = [RelevantIntent]() + + for date in await sunTimes { + let config = CountDownConfiguration() + config.target = .sunriseSet + let relevantContext = RelevantContext.date(from: date - 3600, to: date + 900) + let relevantIntent = RelevantIntent(config, widgetKind: rectWidgetKind, relevance: relevantContext) + relevantIntents.append(relevantIntent) + } + + for date in await moonTimes { + let config = CountDownConfiguration() + config.target = .moonriseSet + let relevantContext = RelevantContext.date(from: date - 3600, to: date + 900) + let relevantIntent = RelevantIntent(config, widgetKind: rectWidgetKind, relevance: relevantContext) + relevantIntents.append(relevantIntent) + } + + for date in await solarTerms { + let config = CountDownConfiguration() + config.target = .solarTerms + let solarTermDate = chineseCalendar.copy + solarTermDate.update(time: date) + let relevantContext = RelevantContext.date(from: solarTermDate.startOfDay, to: solarTermDate.startOfNextDay) + let relevantIntent = RelevantIntent(config, widgetKind: rectWidgetKind, relevance: relevantContext) + relevantIntents.append(relevantIntent) + } + + for date in await moonPhases { + let config = CountDownConfiguration() + config.target = .lunarPhases + let lunarPhaseDate = chineseCalendar.copy + lunarPhaseDate.update(time: date) + let relevantContext = RelevantContext.date(from: lunarPhaseDate.startOfDay, to: lunarPhaseDate.startOfNextDay) + let relevantIntent = RelevantIntent(config, widgetKind: rectWidgetKind, relevance: relevantContext) + relevantIntents.append(relevantIntent) + } + + do { + try await RelevantIntentManager.shared.updateRelevantIntents(relevantIntents) + } catch { + print(error.localizedDescription) + } +} + +func nextMoonTimes(chineseCalendar: ChineseCalendar) -> [Date] { + let moonTimes = chineseCalendar.moonTimes.filter { + ($0.name == ChineseCalendar.moonTimeName[0] || $0.name == ChineseCalendar.moonTimeName[2]) && ($0.date > chineseCalendar.time) + }.map{ $0.date } + return moonTimes +} + +func nextSunTimes(chineseCalendar: ChineseCalendar) -> [Date] { + let sunTimes = chineseCalendar.sunTimes.filter { + ($0.name == ChineseCalendar.dayTimeName[1] || $0.name == ChineseCalendar.dayTimeName[3]) && ($0.date > chineseCalendar.time) + }.map{ $0.date } + return sunTimes +} + +func nextSolarTerm(chineseCalendar: ChineseCalendar) -> [Date] { + let nextSolarTerm = chineseCalendar.solarTerms.first { + $0.date > chineseCalendar.time + } + if let next = nextSolarTerm?.date { + return [next] + } else { + return [] + } +} + +func nextMoonPhase(chineseCalendar: ChineseCalendar) -> [Date] { + let nextLunarPhase = chineseCalendar.moonPhases.first { + $0.date > chineseCalendar.time + } + if let next = nextLunarPhase?.date { + return [next] + } else { + return [] + } +} diff --git a/Widget/WatchWidgets/TextDesp.swift b/Widget/WatchWidgets/TextDesp.swift new file mode 100644 index 0000000..9b218b3 --- /dev/null +++ b/Widget/WatchWidgets/TextDesp.swift @@ -0,0 +1,143 @@ +// +// WatchWidget.swift +// WatchWidget +// +// Created by Leo Liu on 5/10/23. +// + +import AppIntents +import SwiftUI +import WidgetKit + +struct TextConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { + static let intentClassName = "SingleLineIntent" + static var title: LocalizedStringResource = "文字" + static var description = IntentDescription("簡單華曆文字") +} + +struct TextProvider: AppIntentTimelineProvider { + 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]() + } + + 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] { + return [ + AppIntentRecommendation(intent: Intent(), description: "華曆"), + ] + } +} + +struct TextEntry: TimelineEntry, ChineseTimeEntry { + let date: Date + let chineseCalendar: ChineseCalendar + let watchLayout: WatchLayout + let relevance: TimelineEntryRelevance? + + init(configuration: TextProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { + date = chineseCalendar.time + self.chineseCalendar = chineseCalendar + self.watchLayout = watchLayout + self.relevance = TimelineEntryRelevance(score: 5, duration: 144) + } +} + +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() + } + } +} + +struct LineWidget: Widget { + static let kind: String = "Date String" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: Self.kind, intent: TextProvider.Intent.self, provider: TextProvider()) { entry in + TextEntryView(entry: entry) + } + .containerBackgroundRemovable() + .configurationDisplayName("華曆文字") + .description("樸素寫就之華曆") + .supportedFamilies([.accessoryInline]) + } +} + +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 new file mode 100644 index 0000000..6cbd8fa --- /dev/null +++ b/Widget/WatchWidgets/WatchWidgetBasic.swift @@ -0,0 +1,257 @@ +// +// WatchWidgetFace.swift +// WatchWidgetExtension +// +// Created by Leo Liu on 5/11/23. +// + +import SwiftUI +import WidgetKit + +struct LineDescription: View { + let text: String + + init(chineseCalendar: ChineseCalendar) { + var text = chineseCalendar.dateString + let holidays = chineseCalendar.holidays + for holiday in holidays[.. start ? end - start : end - start + 1 + + ZStack { + Circle() + .stroke( + Color.white.opacity(0.5), + lineWidth: lineWidth + ) + .padding(lineWidth / 2) + Circle() + .trim(from: 0, to: length) + .stroke( + Color.white, + style: StrokeStyle( + lineWidth: lineWidth, + lineCap: .round + ) + ) + .padding(lineWidth / 2) + .rotationEffect(.radians(CGFloat.pi * 2.0 * (0.25 + start))) + .scaleEffect(CGSize(width: -1, height: 1)) + } + } +} + +struct Circular: View { + @Environment(\.widgetRenderingMode) var widgetRenderingMode + var outer: (start: CGFloat, end: CGFloat) + var inner: (start: CGFloat, end: CGFloat) + var current: CGFloat? + var innerDirection: CGFloat? + var outerGradient: Gradient + var innerGradient: Gradient + var currentColor: Color? + + var body: some View { + let fullColor = widgetRenderingMode == .fullColor + let whiteGradient = Gradient(colors: [.white, .white]) + + GeometryReader { proxy in + let size = proxy.size + ZStack { + AngularGradient(gradient: fullColor ? outerGradient : whiteGradient + , center: .center, angle: .degrees(90)).mask { + CircularLine(lineWidth: min(size.width, size.height) * 0.1, start: outer.start, end: outer.end) + } + .widgetAccentable() + AngularGradient(gradient: fullColor ? innerGradient : whiteGradient + , center: .center, angle: .radians((-0.25 - (innerDirection ?? 0.5)) * CGFloat.pi * 2.0)).mask { + CircularLine(lineWidth: min(size.width, size.height) * 0.1, start: inner.start, end: inner.end) + .frame(width: size.width * 0.7, height: size.height * 0.7) + } + .widgetAccentable() + if let current = current, let currentColor = currentColor { + (fullColor ? currentColor : .white) + .clipShape(Capsule()) + .frame(width: size.width * 0.28, height: min(size.width, size.height) * 0.1) + .position(CGPoint(x: size.width * 0.13, y: size.height / 2)) + .rotationEffect(.radians((current - 0.25) * CGFloat.pi * 2.0)) + .scaleEffect(CGSize(width: -1, height: 1)) + .shadow(color: .black, radius: min(size.width, size.height) * 0.05) + } + } + } + } +} + +private struct RectIconStyle: ViewModifier { + let size: CGSize + + func body(content: Content) -> some View { + content + .scaledToFit() + .widgetAccentable() + .frame(width: size.height * 0.75, height: size.height * 0.75) + .padding(.top, size.height * 0.125) + .padding(.bottom, size.height * 0.125) + .padding(.trailing, size.height * 0.1) + } +} + +struct RectanglePanel: View { + @Environment(\.widgetRenderingMode) var widgetRenderingMode + var icon: IconType + var name: Text + var color: CGColor + var barColor: CGColor + var start: Date? + var end: Date? + + var body: some View { + let fullColor = widgetRenderingMode == .fullColor + + GeometryReader { proxy in + HStack(alignment: .center) { + switch icon { + case .solarTerm(view: let view): + view + .modifier(RectIconStyle(size: proxy.size)) + case .moon(view: let view): + view + .modifier(RectIconStyle(size: proxy.size)) + case .sunrise(view: let view): + view + .modifier(RectIconStyle(size: proxy.size)) + } + + VStack(alignment: .leading) { + name + .foregroundStyle(fullColor ? Color(cgColor: color) : .white) + .lineLimit(1) + .font(.system(size: 20, weight: .bold, design: .rounded)) + .minimumScaleFactor(0.5) + if let _ = start, let end = end { + Text(end, style: .relative) + .fontDesign(.rounded) + .lineLimit(1) + .minimumScaleFactor(0.75) + .foregroundStyle(.secondary) + } else { + Text("永恆無盡", comment: "Unknown time") + .fontDesign(.rounded) + .lineLimit(1) + .minimumScaleFactor(0.75) + .foregroundStyle(.secondary) + } + } + .widgetAccentable() + .frame(maxWidth: .infinity, idealHeight: proxy.size.height) + } + } + } +} + +private struct CurveIconStyle: ViewModifier { + let size: CGSize + let bar: Bar + + init(size: CGSize, @ViewBuilder bar: () -> Bar) { + self.size = size + self.bar = bar() + } + + func body(content: Content) -> some View { + content + .widgetAccentable() + .frame(width: size.width, height: size.height) + .widgetLabel { bar } + } +} + +struct Curve: View { + @Environment(\.widgetRenderingMode) var widgetRenderingMode + var icon: IconType + var barColor: CGColor + var start: Date? + var end: Date? + + var body: some View { + let fullColor = widgetRenderingMode == .fullColor + + GeometryReader { proxy in + let curveStyle = CurveIconStyle(size: proxy.size) { + if let start = start, let end = end { + ProgressView(timerInterval: start ... end, countsDown: false) { + Text(end, style: .relative) + } + .tint(fullColor ? Color(cgColor: barColor) : .white) + .widgetAccentable() + } + } + + switch icon { + case .solarTerm(view: let view): + view + .modifier(curveStyle) + case .moon(view: let view): + view + .modifier(curveStyle) + case .sunrise(view: let view): + view + .modifier(curveStyle) + } + } + } +} + +struct CalendarBadge: View { + + let dateString: String + let timeString: String + let color: Gradient + let backGround: Color + @Environment(\.widgetRenderingMode) var widgetRenderingMode + + private func prepareText(_ text: String, size: CGFloat) -> Text { + let attrStr = NSMutableAttributedString(string: String(text.reversed())) + let centerFont = WatchLayout.shared.centerFont.withSize(size) + attrStr.addAttributes([.font: centerFont, .foregroundColor: CGColor(gray: 1, alpha: 1)], range: NSMakeRange(0, attrStr.length)) + return Text(AttributedString(attrStr)) + } + + var body: some View { + let fullColor = widgetRenderingMode == .fullColor + let whiteGradient = Gradient(colors: [.white, .white]) + + GeometryReader { proxy in + VStack{ + let fontSize = min(proxy.size.width / 8, proxy.size.height / 3) + prepareText(dateString, size: fontSize) + .multilineTextAlignment(.center) + prepareText(timeString, size: fontSize) + .multilineTextAlignment(.center) + } + .padding(.bottom, 1) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundStyle(LinearGradient(gradient: fullColor ? color : whiteGradient, + startPoint: .bottomLeading, endPoint: .topTrailing)) + .widgetAccentable() + } + } +} diff --git a/Widget/Widget.swift b/Widget/Widget.swift deleted file mode 100644 index 3bae5f6..0000000 --- a/Widget/Widget.swift +++ /dev/null @@ -1,289 +0,0 @@ -// -// Widget.swift -// iOSWidgetExtension -// -// Created by Leo Liu on 5/9/23. -// - -import Intents -import SwiftUI -import WidgetKit - -struct SmallProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> SmallEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - return SmallEntry(date: Date(), configuration: SmallIntent(), chineseCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: SmallIntent, in context: Context, completion: @escaping (SmallEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - let entry = SmallEntry(date: Date(), configuration: configuration, chineseCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: SmallIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [SmallEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - switch configuration.mode { - case .time: -#if os(macOS) - let count = 50 -#else - let count = 15 -#endif - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for offset in 0 ..< count { - let entryDate = currentDate + Double(offset * 864) // 14.4min - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = SmallEntry(date: entryDate, configuration: configuration, chineseCalendar: chineseCalendar.copy) - entries.append(entry) - } - default: -#if os(macOS) - let count = 24 -#else - let count = 15 -#endif - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for offset in 0 ..< count { - let entryDate = Calendar.current.date(byAdding: .hour, value: offset, to: currentDate)! - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = SmallEntry(date: entryDate, configuration: configuration, chineseCalendar: chineseCalendar.copy) - entries.append(entry) - } - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } -} - -struct MediumProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> MediumEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - return MediumEntry(date: Date(), configuration: MediumIntent(), chineseCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: MediumIntent, in context: Context, completion: @escaping (MediumEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - let entry = MediumEntry(date: Date(), configuration: configuration, chineseCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: MediumIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [MediumEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - -#if os(macOS) - let count = 50 -#else - let count = 15 -#endif - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - for offset in 0 ..< count { - let entryDate = currentDate + Double(offset * 864) // 14.4min - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = MediumEntry(date: entryDate, configuration: configuration, chineseCalendar: chineseCalendar.copy) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } -} - -struct LargeProvider: IntentTimelineProvider { - func placeholder(in context: Context) -> LargeEntry { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: false) - return LargeEntry(date: Date(), configuration: LargeIntent(), chineseCalendar: chineseCalendar) - } - - func getSnapshot(for configuration: LargeIntent, in context: Context, completion: @escaping (LargeEntry) -> ()) { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: false) - let entry = LargeEntry(date: Date(), configuration: configuration, chineseCalendar: chineseCalendar) - completion(entry) - } - - func getTimeline(for configuration: LargeIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [LargeEntry] = [] - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() -#if os(macOS) - let count = 50 -#else - let count = 15 -#endif - let chineseCalendar = ChineseCalendar(time: currentDate, timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: false) - for offset in 0 ..< count { - let entryDate = currentDate + Double(offset * 864) // 14.4min - chineseCalendar.update(time: entryDate, timezone: chineseCalendar.calendar.timeZone, location: chineseCalendar.location) - let entry = LargeEntry(date: entryDate, configuration: configuration, chineseCalendar: chineseCalendar.copy) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } -} - -struct SmallEntry: TimelineEntry { - let date: Date - let configuration: SmallIntent - let chineseCalendar: ChineseCalendar -} - -struct MediumEntry: TimelineEntry { - let date: Date - let configuration: MediumIntent - let chineseCalendar: ChineseCalendar -} - -struct LargeEntry: TimelineEntry { - let date: Date - let configuration: LargeIntent - let chineseCalendar: ChineseCalendar -} - -struct SmallWidgetEntryView: View { - var entry: SmallProvider.Entry - - init(entry: SmallEntry) { - self.entry = entry - } - - var body: some View { - ZStack { - if let opacity = entry.configuration.backAlpha?.doubleValue { - Color.gray.opacity(opacity) - } - switch entry.configuration.mode { - case .time: - TimeWatch(compact: true, chineseCalendar: entry.chineseCalendar) - default: - DateWatch(compact: true, chineseCalendar: entry.chineseCalendar) - } - } - } -} - -struct MediumWidgetEntryView: View { - var entry: MediumProvider.Entry - - init(entry: MediumEntry) { - self.entry = entry - } - - var body: some View { - GeometryReader { proxy in - ZStack { - if let opacity = entry.configuration.backAlpha?.doubleValue { - Color.gray.opacity(opacity) - } - switch entry.configuration.order { - case .dateRight: - HStack(spacing: (proxy.size.width - proxy.size.height * 2) * 0.5) { - TimeWatch(compact: true, chineseCalendar: entry.chineseCalendar) - DateWatch(compact: true, chineseCalendar: entry.chineseCalendar) - } - .padding(.horizontal, (proxy.size.width - proxy.size.height * 2) * 0.25) - default: - HStack(spacing: (proxy.size.width - proxy.size.height * 2) * 0.5) { - DateWatch(compact: true, chineseCalendar: entry.chineseCalendar) - TimeWatch(compact: true, chineseCalendar: entry.chineseCalendar) - } - .padding(.horizontal, (proxy.size.width - proxy.size.height * 2) * 0.25) - } - } - } - } -} - -struct LargeWidgetEntryView: View { - var entry: LargeProvider.Entry - - init(entry: LargeEntry) { - self.entry = entry - } - - var body: some View { - ZStack { - if let opacity = entry.configuration.backAlpha?.doubleValue { - Color.gray.opacity(opacity) - } - Watch(compact: false, chineseCalendar: entry.chineseCalendar) - } - } -} - -struct SmallWidget: Widget { - let kind: String = "Small" - - init() { - DataContainer.shared.loadSave() - LocationManager.shared.manager.requestWhenInUseAuthorization() - } - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: SmallIntent.self, provider: SmallProvider()) { entry in - SmallWidgetEntryView(entry: entry) - } - .configurationDisplayName("Compact") - .description("Compact watch face to display either Date or Time.") - .supportedFamilies([.systemSmall]) - } -} - -struct MediumWidget: Widget { - let kind: String = "Medium" - - init() { - DataContainer.shared.loadSave() - LocationManager.shared.manager.requestWhenInUseAuthorization() - } - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: MediumIntent.self, provider: MediumProvider()) { entry in - MediumWidgetEntryView(entry: entry) - } - .configurationDisplayName("Dual") - .description("Display both Date and Time as separate watches, whose order is at your choice.") - .supportedFamilies([.systemMedium]) - } -} - -struct LargeWidget: Widget { - let kind: String = "Large" - - init() { - DataContainer.shared.loadSave() - LocationManager.shared.manager.requestWhenInUseAuthorization() - } - - var body: some WidgetConfiguration { - IntentConfiguration(kind: kind, intent: LargeIntent.self, provider: LargeProvider()) { entry in - LargeWidgetEntryView(entry: entry) - } - .configurationDisplayName("Full") - .description("Display full information with both Date and Time.") - .supportedFamilies([.systemLarge]) - } -} - -struct Widget_Previews: PreviewProvider { - static var previews: some View { - let chineseCalendar = ChineseCalendar(time: Date(), timezone: Calendar.current.timeZone, location: LocationManager.shared.location ?? WatchLayout.shared.location, compact: true) - SmallWidgetEntryView(entry: SmallEntry(date: Date(), configuration: SmallIntent(), chineseCalendar: chineseCalendar)) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - } -} diff --git a/Widget/WidgetFaceView.swift b/Widget/WidgetFaceView.swift deleted file mode 100644 index 340c0ec..0000000 --- a/Widget/WidgetFaceView.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// WatchFaceView.swift -// MacWidgetExtension -// -// Created by Leo Liu on 5/9/23. -// - -import SwiftUI - -private func calSubhourGradient(watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) -> WatchLayout.Gradient { - let startOfDay = chineseCalendar.startOfDay - let lengthOfDay = startOfDay.distance(to: chineseCalendar.startOfNextDay) - let fourthRingColor = WatchLayout.Gradient(locations: [0, 1], colors: [ - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.startHour) / lengthOfDay) % 1.0), - watchLayout.thirdRing.interpolate(at: (startOfDay.distance(to: chineseCalendar.endHour) / lengthOfDay) % 1.0) - ], loop: false) - return fourthRingColor -} - -enum Rings { - case date - case time -} - -private func ringMarks(for ring: Rings, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar, radius: CGFloat) -> ([Marks], [Marks]) { - switch ring { - case .date: - let eventInMonth = chineseCalendar.eventInMonth - let firstRingMarks = [Marks(outer: true, locations: chineseCalendar.planetPosition.map { $0.pos }, colors: watchLayout.planetIndicator, radius: radius)] - let secondRingMarks = [ - Marks(outer: true, locations: eventInMonth.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInMonth.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius) - ] - return (firstRingMarks, secondRingMarks) - - case .time: - let eventInDay = chineseCalendar.eventInDay - let sunMoonPositions = chineseCalendar.sunMoonPositions - let thirdRingMarks = [ - Marks(outer: true, locations: eventInDay.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInDay.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius), - Marks(outer: false, locations: sunMoonPositions.solar.map { option in option.map { value in value.pos } }, colors: watchLayout.sunPositionIndicator, radius: radius), - Marks(outer: false, locations: sunMoonPositions.lunar.map { option in option.map { value in value.pos } }, colors: watchLayout.moonPositionIndicator, radius: radius) - ] - let eventInHour = chineseCalendar.eventInHour - let sunMoonSubhourPositions = chineseCalendar.sunMoonSubhourPositions - let fourthRingMarks = [ - Marks(outer: true, locations: eventInHour.eclipse.map { $0.pos }, colors: [watchLayout.eclipseIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.fullMoon.map { $0.pos }, colors: [watchLayout.fullmoonIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.oddSolarTerm.map { $0.pos }, colors: [watchLayout.oddStermIndicator], radius: radius), - Marks(outer: true, locations: eventInHour.evenSolarTerm.map { $0.pos }, colors: [watchLayout.evenStermIndicator], radius: radius), - Marks(outer: false, locations: sunMoonSubhourPositions.solar.map { option in option.map { value in value.pos } }, colors: watchLayout.sunPositionIndicator, radius: radius), - Marks(outer: false, locations: sunMoonSubhourPositions.lunar.map { option in option.map { value in value.pos } }, colors: watchLayout.moonPositionIndicator, radius: radius) - ] - return (thirdRingMarks, fourthRingMarks) - } -} - -struct Watch: View { - @Environment(\.colorScheme) var colorScheme - static let frameOffset: CGFloat = 0.05 - - @State var size = CGSize.zero - let compact: Bool - let phase = StartingPhase() - let watchLayout: WatchLayout - let chineseCalendar: ChineseCalendar - - init(compact: Bool, chineseCalendar: ChineseCalendar) { - self.compact = compact - self.watchLayout = WatchLayout.shared - self.chineseCalendar = chineseCalendar - } - - var body: some View { - let shortEdge = min(self.size.width, self.size.height) - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: Self.frameOffset * shortEdge) - let firstRingOuter = outerBound.shrink(by: ZeroRing.width * shortEdge * 0.8) - let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 0.8) - let thirdRingOuter = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 0.8) - let fourthRingOuter = thirdRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 0.8) - let innerBound = fourthRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 0.8) - let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) - - let (firstRingMarks, secondRingMarks) = ringMarks(for: .date, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 0.7) - let (thirdRingMarks, fourthRingMarks) = ringMarks(for: .time, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 0.7) - - let shadowDirection = chineseCalendar.currentHourInDay - - let oddSTColor = colorScheme == .dark ? watchLayout.oddSolarTermTickColorDark : watchLayout.oddSolarTermTickColor - let evenSTColor = colorScheme == .dark ? watchLayout.evenSolarTermTickColorDark : watchLayout.evenSolarTermTickColor - let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor - let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor - let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor - let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor - - GeometryReader { proxy in - ZStack { - ZeroRing(width: ZeroRing.width * 0.8, 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) - Ring(width: Ring.paddedWidth * 0.8, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 0.8, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 0.8, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 0.8, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection) - Core(viewSize: size, compact: compact, dateString: chineseCalendar.dateString, timeString: chineseCalendar.hourString + chineseCalendar.shortQuarterString, font: WatchFont(watchLayout.centerFont), maxLength: 5, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: coreColor, centerOffset: 0.1, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - } - .ignoresSafeArea(edges: .bottom) - } -} - -struct DateWatch: View { - @Environment(\.colorScheme) var colorScheme - static let frameOffset: CGFloat = 0.07 - - @State var size = CGSize.zero - let compact: Bool - let phase = StartingPhase() - let watchLayout: WatchLayout - let chineseCalendar: ChineseCalendar - - init(compact: Bool, chineseCalendar: ChineseCalendar) { - self.compact = compact - self.watchLayout = WatchLayout.shared - self.chineseCalendar = chineseCalendar - } - - var body: some View { - let shortEdge = min(self.size.width, self.size.height) - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: Self.frameOffset * shortEdge) - let firstRingOuter = outerBound - let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - - let (firstRingMarks, secondRingMarks) = ringMarks(for: .date, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 1.5) - - let shadowDirection = chineseCalendar.currentHourInDay - - let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor - let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor - let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor - let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor - - GeometryReader { proxy in - ZStack { - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection) - Core(viewSize: size, compact: compact, dateString: chineseCalendar.monthString, timeString: chineseCalendar.dayString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: coreColor, centerOffset: 0.1, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - } - .ignoresSafeArea(edges: .bottom) - } -} - -struct TimeWatch: View { - @Environment(\.colorScheme) var colorScheme - static let frameOffset: CGFloat = 0.07 - - @State var size = CGSize.zero - let compact: Bool - let phase = StartingPhase() - let watchLayout: WatchLayout - let chineseCalendar: ChineseCalendar - - init(compact: Bool, chineseCalendar: ChineseCalendar) { - self.compact = compact - self.watchLayout = WatchLayout.shared - self.chineseCalendar = chineseCalendar - } - - var body: some View { - let shortEdge = min(self.size.width, self.size.height) - let cornerSize = watchLayout.cornerRadiusRatio * shortEdge - let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2).shrink(by: Self.frameOffset * shortEdge) - let firstRingOuter = outerBound - let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * 1.3) - let fourthRingColor = calSubhourGradient(watchLayout: watchLayout, chineseCalendar: chineseCalendar) - - let (firstRingMarks, secondRingMarks) = ringMarks(for: .time, watchLayout: watchLayout, chineseCalendar: chineseCalendar, radius: Marks.markSize * shortEdge * 1.5) - - let shadowDirection = chineseCalendar.currentHourInDay - - let textColor = colorScheme == .dark ? watchLayout.fontColorDark : watchLayout.fontColor - let majorTickColor = colorScheme == .dark ? watchLayout.majorTickColorDark : watchLayout.majorTickColor - let minorTickColor = colorScheme == .dark ? watchLayout.minorTickColorDark : watchLayout.minorTickColor - let coreColor = colorScheme == .dark ? watchLayout.innerColorDark : watchLayout.innerColor - - GeometryReader { proxy in - ZStack { - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection) - Ring(width: Ring.paddedWidth * 1.3, viewSize: size, compact: compact, cornerSize: watchLayout.cornerRadiusRatio, 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, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection) - Core(viewSize: size, compact: compact, dateString: chineseCalendar.hourString, timeString: chineseCalendar.shortQuarterString, font: WatchFont(watchLayout.centerFont), maxLength: 3, textColor: watchLayout.centerFontColor, outerBound: innerBound, backColor: coreColor, centerOffset: 0.1, shadowDirection: shadowDirection) - } - .onAppear { - size = proxy.size - } - } - .ignoresSafeArea(edges: .bottom) - } -} diff --git a/Widget/en.lproj/Widget.strings b/Widget/en.lproj/Widget.strings deleted file mode 100644 index 47bf4f4..0000000 --- a/Widget/en.lproj/Widget.strings +++ /dev/null @@ -1,129 +0,0 @@ -/* (No Comment) */ -"02xN5Y" = "Background Alpha"; - -/* (No Comment) */ -"4MUCeV" = "${displayName} can’t be lower than ${minimumValue}."; - -/* (No Comment) */ -"A2Gu23-F0IoDR" = "There are ${count} options matching ‘Default’."; - -/* (No Comment) */ -"A2Gu23-oeabNo" = "There are ${count} options matching ‘Time’."; - -/* (No Comment) */ -"A2Gu23-qgxKar" = "There are ${count} options matching ‘Date’."; - -/* (No Comment) */ -"AofoHj" = "Date Left"; - -/* (No Comment) */ -"BW541T" = "${displayName} can’t be lower than ${minimumValue}."; - -/* (No Comment) */ -"EUjqke" = "${displayName} can’t be higher than ${maximumValue}."; - -/* (No Comment) */ -"F0IoDR" = "Default"; - -/* (No Comment) */ -"FJbM3s-AofoHj" = "Just to confirm, you wanted ‘Date Left’?"; - -/* (No Comment) */ -"FJbM3s-jNk9HH" = "Just to confirm, you wanted ‘Date Right’?"; - -/* (No Comment) */ -"FJbM3s-XhZUWJ" = "Just to confirm, you wanted ‘default’?"; - -/* (No Comment) */ -"FP4Udk" = "Large widget options"; - -/* (No Comment) */ -"GeXaBx" = "Medium"; - -/* (No Comment) */ -"gpCwrM" = "Small"; - -/* (No Comment) */ -"gQ74GO" = "Display Mode"; - -/* (No Comment) */ -"jhRO44" = "${displayName} can’t be higher than ${maximumValue}."; - -/* (No Comment) */ -"jNk9HH" = "Date Right"; - -/* (No Comment) */ -"LzSe8F" = "Display Order"; - -/* (No Comment) */ -"O25A4M" = "${displayName} can’t be lower than ${minimumValue}."; - -/* (No Comment) */ -"oeabNo" = "Time"; - -/* (No Comment) */ -"oMSedJ" = "Set a value for background transparancy"; - -/* (No Comment) */ -"QaJIBA" = "Background Alpha"; - -/* (No Comment) */ -"qgxKar" = "Date"; - -/* (No Comment) */ -"QiDXla" = "Set a value for background transparancy"; - -/* (No Comment) */ -"rnexwt-F0IoDR" = "Just to confirm, you wanted ‘Default’?"; - -/* (No Comment) */ -"rnexwt-oeabNo" = "Just to confirm, you wanted ‘Time’?"; - -/* (No Comment) */ -"rnexwt-qgxKar" = "Just to confirm, you wanted ‘Date’?"; - -/* (No Comment) */ -"tuT0Ze" = "${displayName} can’t be negative."; - -/* (No Comment) */ -"tVvJ9c" = "Small widget options"; - -/* (No Comment) */ -"Uh8qA0" = "Large"; - -/* (No Comment) */ -"uq6y5D" = "${displayName} can’t be higher than ${maximumValue}."; - -/* (No Comment) */ -"v0khyr" = "${displayName} can’t be negative."; - -/* (No Comment) */ -"v50Klc-AofoHj" = "There are ${count} options matching ‘Date Left’."; - -/* (No Comment) */ -"v50Klc-jNk9HH" = "There are ${count} options matching ‘Date Right’."; - -/* (No Comment) */ -"v50Klc-XhZUWJ" = "There are ${count} options matching ‘default’."; - -/* (No Comment) */ -"vouhqV" = "Background Alpha"; - -/* (No Comment) */ -"vyyVxE" = "Medium widget options"; - -/* (No Comment) */ -"vzqK2A" = "Set a value for background transparancy"; - -/* (No Comment) */ -"xetndN" = "Display Order"; - -/* (No Comment) */ -"XhZUWJ" = "default"; - -/* (No Comment) */ -"xJGaGp" = "${displayName} can’t be negative."; - -/* (No Comment) */ -"XwYYHO" = "Display Mode"; - diff --git a/Widget/zh-Hans.lproj/Widget.strings b/Widget/zh-Hans.lproj/Widget.strings deleted file mode 100644 index ccd03fc..0000000 --- a/Widget/zh-Hans.lproj/Widget.strings +++ /dev/null @@ -1,129 +0,0 @@ -/* (No Comment) */ -"02xN5Y" = "背景透明"; - -/* (No Comment) */ -"4MUCeV" = "${displayName}不可低于${minimumValue}。"; - -/* (No Comment) */ -"A2Gu23-F0IoDR" = "共有${count}种‘默认’项。"; - -/* (No Comment) */ -"A2Gu23-oeabNo" = "共有${count} 种‘仅时’项。"; - -/* (No Comment) */ -"A2Gu23-qgxKar" = "共有${count}种‘仅日’项。"; - -/* (No Comment) */ -"AofoHj" = "日在左"; - -/* (No Comment) */ -"BW541T" = "${displayName}不可低于${minimumValue}。"; - -/* (No Comment) */ -"EUjqke" = "${displayName}不可高于${maximumValue}。"; - -/* (No Comment) */ -"F0IoDR" = "默认"; - -/* (No Comment) */ -"FJbM3s-AofoHj" = "确认选‘日在左’吗?"; - -/* (No Comment) */ -"FJbM3s-jNk9HH" = "确认选‘日在右’吗?"; - -/* (No Comment) */ -"FJbM3s-XhZUWJ" = "确认选‘默认’吗?"; - -/* (No Comment) */ -"FP4Udk" = "大挂件选择"; - -/* (No Comment) */ -"GeXaBx" = "中"; - -/* (No Comment) */ -"gpCwrM" = "小"; - -/* (No Comment) */ -"gQ74GO" = "类型"; - -/* (No Comment) */ -"jhRO44" = "${displayName}不可高于${maximumValue}。"; - -/* (No Comment) */ -"jNk9HH" = "日在右"; - -/* (No Comment) */ -"LzSe8F" = "顺序"; - -/* (No Comment) */ -"O25A4M" = "${displayName}不可低于${minimumValue}。"; - -/* (No Comment) */ -"oeabNo" = "仅时"; - -/* (No Comment) */ -"oMSedJ" = "设背景透明度"; - -/* (No Comment) */ -"QaJIBA" = "背景透明"; - -/* (No Comment) */ -"qgxKar" = "仅日"; - -/* (No Comment) */ -"QiDXla" = "设背景透明度"; - -/* (No Comment) */ -"rnexwt-F0IoDR" = "确认选‘默认’吗?"; - -/* (No Comment) */ -"rnexwt-oeabNo" = "确认选‘仅时’吗?"; - -/* (No Comment) */ -"rnexwt-qgxKar" = "确认选‘仅日’吗?"; - -/* (No Comment) */ -"tuT0Ze" = "${displayName}不可为负。"; - -/* (No Comment) */ -"tVvJ9c" = "小挂件选项"; - -/* (No Comment) */ -"Uh8qA0" = "大"; - -/* (No Comment) */ -"uq6y5D" = "${displayName}不可高于${maximumValue}。"; - -/* (No Comment) */ -"v0khyr" = "${displayName}不可为负。"; - -/* (No Comment) */ -"v50Klc-AofoHj" = "共有${count} 种‘日在左’项。"; - -/* (No Comment) */ -"v50Klc-jNk9HH" = "共有${count} 种‘日在右’项。"; - -/* (No Comment) */ -"v50Klc-XhZUWJ" = "共有${count} 种‘默认’项。"; - -/* (No Comment) */ -"vouhqV" = "背景透明"; - -/* (No Comment) */ -"vyyVxE" = "中挂件选项"; - -/* (No Comment) */ -"vzqK2A" = "设背景透明度"; - -/* (No Comment) */ -"xetndN" = "顺序"; - -/* (No Comment) */ -"XhZUWJ" = "默认"; - -/* (No Comment) */ -"xJGaGp" = "${displayName}不可为负。"; - -/* (No Comment) */ -"XwYYHO" = "类型"; - diff --git a/Widget/zh-Hant.lproj/Widget.strings b/Widget/zh-Hant.lproj/Widget.strings deleted file mode 100644 index 29163d3..0000000 --- a/Widget/zh-Hant.lproj/Widget.strings +++ /dev/null @@ -1,129 +0,0 @@ -/* (No Comment) */ -"02xN5Y" = "背景透明"; - -/* (No Comment) */ -"4MUCeV" = "${displayName}不可低於${minimumValue}。"; - -/* (No Comment) */ -"A2Gu23-F0IoDR" = "共有${count}種「默認」項。"; - -/* (No Comment) */ -"A2Gu23-oeabNo" = "共有${count}種「僅時」項。"; - -/* (No Comment) */ -"A2Gu23-qgxKar" = "共有${count}種「僅日」項。"; - -/* (No Comment) */ -"AofoHj" = "日在左"; - -/* (No Comment) */ -"BW541T" = "${displayName}不可低於${minimumValue}。"; - -/* (No Comment) */ -"EUjqke" = "${displayName}不可高於${maximumValue}。"; - -/* (No Comment) */ -"F0IoDR" = "默認"; - -/* (No Comment) */ -"FJbM3s-AofoHj" = "確認選「日在左」嗎?"; - -/* (No Comment) */ -"FJbM3s-jNk9HH" = "確認選「日在右」嗎?"; - -/* (No Comment) */ -"FJbM3s-XhZUWJ" = "確認選「默認」嗎?"; - -/* (No Comment) */ -"FP4Udk" = "大掛件選項"; - -/* (No Comment) */ -"GeXaBx" = "中"; - -/* (No Comment) */ -"gpCwrM" = "小"; - -/* (No Comment) */ -"gQ74GO" = "類型"; - -/* (No Comment) */ -"jhRO44" = "${displayName}不可高於${maximumValue}。"; - -/* (No Comment) */ -"jNk9HH" = "日在右"; - -/* (No Comment) */ -"LzSe8F" = "順序"; - -/* (No Comment) */ -"O25A4M" = "${displayName}不可低於${minimumValue}。"; - -/* (No Comment) */ -"oeabNo" = "僅時"; - -/* (No Comment) */ -"oMSedJ" = "設背景透明度"; - -/* (No Comment) */ -"QaJIBA" = "背景透明"; - -/* (No Comment) */ -"qgxKar" = "僅日"; - -/* (No Comment) */ -"QiDXla" = "設背景透明度"; - -/* (No Comment) */ -"rnexwt-F0IoDR" = "確認選「默認」嗎?"; - -/* (No Comment) */ -"rnexwt-oeabNo" = "確認選「僅時」嗎?"; - -/* (No Comment) */ -"rnexwt-qgxKar" = "確認選「僅日」嗎?"; - -/* (No Comment) */ -"tuT0Ze" = "${displayName}不可爲負。"; - -/* (No Comment) */ -"tVvJ9c" = "小掛件選項"; - -/* (No Comment) */ -"Uh8qA0" = "大"; - -/* (No Comment) */ -"uq6y5D" = "${displayName}不可高於${maximumValue}。"; - -/* (No Comment) */ -"v0khyr" = "${displayName}不可爲負。"; - -/* (No Comment) */ -"v50Klc-AofoHj" = "共有${count}種「日在左」項。"; - -/* (No Comment) */ -"v50Klc-jNk9HH" = "共有${count}種「日在右」項。"; - -/* (No Comment) */ -"v50Klc-XhZUWJ" = "共有${count}種「默認」項。"; - -/* (No Comment) */ -"vouhqV" = "背景透明"; - -/* (No Comment) */ -"vyyVxE" = "中掛件選項"; - -/* (No Comment) */ -"vzqK2A" = "設背景透明度"; - -/* (No Comment) */ -"xetndN" = "順序"; - -/* (No Comment) */ -"XhZUWJ" = "默認"; - -/* (No Comment) */ -"xJGaGp" = "${displayName}不可爲負。"; - -/* (No Comment) */ -"XwYYHO" = "類型"; - diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift deleted file mode 100644 index 5cba598..0000000 --- a/iOS/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// Chinese Time -// -// Created by Leo Liu on 4/17/23. -// - -import UIKit - -@main -final class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - DataContainer.shared.loadSave() - - LocationManager.shared.requestLocation { _ in - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } -} diff --git a/iOS/Assets.xcassets/AppIcon.appiconset/iOS.png b/iOS/Assets.xcassets/AppIcon.appiconset/iOS.png index 7718c91..dadd614 100644 Binary files a/iOS/Assets.xcassets/AppIcon.appiconset/iOS.png and b/iOS/Assets.xcassets/AppIcon.appiconset/iOS.png differ diff --git a/iOS/Assets.xcassets/Contents.json b/iOS/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/iOS/Assets.xcassets/Contents.json +++ b/iOS/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/iOS/Assets.xcassets/Image.imageset/Contents.json b/iOS/Assets.xcassets/Image.imageset/Contents.json index 87cfca0..273cf1b 100644 --- a/iOS/Assets.xcassets/Image.imageset/Contents.json +++ b/iOS/Assets.xcassets/Image.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "mac 128.png", + "filename" : "icon 120.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "mac 256.png", + "filename" : "icon 240.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "mac512.png", + "filename" : "icon 360.png", "idiom" : "universal", "scale" : "3x" } diff --git a/iOS/Assets.xcassets/Image.imageset/icon 120.png b/iOS/Assets.xcassets/Image.imageset/icon 120.png new file mode 100644 index 0000000..8b0eda0 Binary files /dev/null and b/iOS/Assets.xcassets/Image.imageset/icon 120.png differ diff --git a/iOS/Assets.xcassets/Image.imageset/icon 240.png b/iOS/Assets.xcassets/Image.imageset/icon 240.png new file mode 100644 index 0000000..279a007 Binary files /dev/null and b/iOS/Assets.xcassets/Image.imageset/icon 240.png differ diff --git a/iOS/Assets.xcassets/Image.imageset/icon 360.png b/iOS/Assets.xcassets/Image.imageset/icon 360.png new file mode 100644 index 0000000..5c14859 Binary files /dev/null and b/iOS/Assets.xcassets/Image.imageset/icon 360.png differ diff --git a/iOS/Assets.xcassets/Image.imageset/mac 128.png b/iOS/Assets.xcassets/Image.imageset/mac 128.png deleted file mode 100644 index c3bafdb..0000000 Binary files a/iOS/Assets.xcassets/Image.imageset/mac 128.png and /dev/null differ diff --git a/iOS/Assets.xcassets/Image.imageset/mac 256.png b/iOS/Assets.xcassets/Image.imageset/mac 256.png deleted file mode 100644 index 9ecd187..0000000 Binary files a/iOS/Assets.xcassets/Image.imageset/mac 256.png and /dev/null differ diff --git a/iOS/Assets.xcassets/Image.imageset/mac512.png b/iOS/Assets.xcassets/Image.imageset/mac512.png deleted file mode 100644 index bac904b..0000000 Binary files a/iOS/Assets.xcassets/Image.imageset/mac512.png and /dev/null differ diff --git a/iOS/Assets.xcassets/groupBack.colorset/Contents.json b/iOS/Assets.xcassets/groupBack.colorset/Contents.json deleted file mode 100644 index 7fd4b86..0000000 --- a/iOS/Assets.xcassets/groupBack.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x2C", - "red" : "0x2C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/Assets.xcassets/tableBack.colorset/Contents.json b/iOS/Assets.xcassets/tableBack.colorset/Contents.json deleted file mode 100644 index 7021ce2..0000000 --- a/iOS/Assets.xcassets/tableBack.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF2", - "red" : "0xF2" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0x1E", - "green" : "0x1C", - "red" : "0x1C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard deleted file mode 100644 index 54981b3..0000000 --- a/iOS/Base.lproj/Main.storyboard +++ /dev/null @@ -1,1495 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOS/ChineseTime.xcdatamodeld/.xccurrentversion b/iOS/ChineseTime.xcdatamodeld/.xccurrentversion deleted file mode 100644 index 0c67376..0000000 --- a/iOS/ChineseTime.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/iOS/ChineseTime.xcdatamodeld/Chinese_Time.xcdatamodel/contents b/iOS/ChineseTime.xcdatamodeld/Chinese_Time.xcdatamodel/contents deleted file mode 100644 index 211f48c..0000000 --- a/iOS/ChineseTime.xcdatamodeld/Chinese_Time.xcdatamodel/contents +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/iOS/Chinese Time iOS.entitlements b/iOS/ChineseTimeiOS.entitlements similarity index 100% rename from iOS/Chinese Time iOS.entitlements rename to iOS/ChineseTimeiOS.entitlements diff --git a/iOS/Info.plist b/iOS/Info.plist index 4c81eef..d1850bb 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -4,12 +4,15 @@ ITSAppUsesNonExemptEncryption + LSHasLocalizedDisplayName + NSUserActivityTypes CircularIntent CurveIntent LargeIntent MediumIntent + RectIntent SingleLineIntent SmallIntent @@ -24,21 +27,17 @@ UISceneConfigurations UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - + UIBackgroundModes remote-notification + UILaunchScreen + + UIImageName + image + diff --git a/iOS/LaunchScreen.storyboard b/iOS/LaunchScreen.storyboard deleted file mode 100644 index 865e932..0000000 --- a/iOS/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOS/Layout.swift b/iOS/Layout.swift index 11f4143..1ae3ea3 100644 --- a/iOS/Layout.swift +++ b/iOS/Layout.swift @@ -5,17 +5,37 @@ // Created by LEO Yoon-Tsaw on 9/23/21. // -import UIKit +import SwiftUI +import Observation -final class WatchLayout: MetaWatchLayout { +@Observable final class WatchLayout: MetaWatchLayout { static let shared = WatchLayout() - var textFont: UIFont - var centerFont: UIFont - - override init() { - textFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .regular) - centerFont = UIFont(name: "SourceHanSansKR-Heavy", size: UIFont.systemFontSize)! + var textFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .regular) + var centerFont = UIFont(name: "SourceHanSansKR-Heavy", size: UIFont.systemFontSize)! + + private override init() { super.init() } + + var monochrome: Self { + let emptyLayout = Self.init() + emptyLayout.update(from: self.encode(includeColor: false)) + return emptyLayout + } + + func binding(_ keyPath: ReferenceWritableKeyPath) -> Binding { + return Binding(get: { self[keyPath: keyPath] }, set: { self[keyPath: keyPath] = $0 }) + } +} + +@Observable class WatchSetting { + static let shared = WatchSetting() + + var displayTime: Date? = nil + var timezone: TimeZone? = nil + var presentSetting = false + var vertical = true + + private init() {} } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift deleted file mode 100644 index a4db459..0000000 --- a/iOS/SceneDelegate.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// SceneDelegate.swift -// Chinese Time -// -// Created by Leo Liu on 4/17/23. -// - -import UIKit -import WidgetKit - -final class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - DataContainer.shared.saveLayout(WatchLayout.shared.encode()) - WidgetCenter.shared.reloadAllTimelines() - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Save changes in the application's managed object context when the application transitions to the background. - DataContainer.shared.saveContext() - } -} diff --git a/iOS/ViewController.swift b/iOS/ViewController.swift deleted file mode 100644 index c5154ad..0000000 --- a/iOS/ViewController.swift +++ /dev/null @@ -1,1808 +0,0 @@ -// -// ViewController.swift -// Chinese Time -// -// Created by Leo Liu on 4/17/23. -// - -import UIKit - -private func coordinateDesp(coordinate: CGPoint) -> (String, String) { - var latitudeLabel = "" - if coordinate.x > 0 { - latitudeLabel = "N" - } else if coordinate.x < 0 { - latitudeLabel = "S" - } - let latitude = Int(round(abs(coordinate.x) * 3600)) - let latitudeString = "\(latitude / 3600)°\((latitude % 3600) / 60)\'\(latitude % 60)\" \(latitudeLabel)" - - var longitudeLabel = "" - if coordinate.y > 0 { - longitudeLabel = "E" - } else if coordinate.y < 0 { - longitudeLabel = "W" - } - let longitude = Int(round(abs(coordinate.y) * 3600)) - let longitudeString = "\(longitude / 3600)°\((longitude % 3600) / 60)\'\(longitude % 60)\" \(longitudeLabel)" - - return (latitudeString, longitudeString) -} - -extension UINavigationController { - @objc func closeSetting(_ sender: UIView) { - dismiss(animated: true) - } -} - -final class ViewController: UIViewController, UIGestureRecognizerDelegate { - @IBOutlet var watchFace: WatchFaceView! - private var tooltipView: NoteView? - - func newSize(frame: CGSize, idealSize: CGSize) -> CGSize { - let height: CGFloat - let width: CGFloat - if frame.width > frame.height { - height = min(frame.height, idealSize.width) - width = min(idealSize.height / idealSize.width * height, frame.width * 0.8) - } else { - width = min(frame.width, idealSize.width) - height = min(idealSize.height / idealSize.width * width, frame.height * 0.8) - } - return CGSize(width: width, height: height) - } - - func resize() { - let screen = UIScreen.main.bounds - let newSize = newSize(frame: screen.size, idealSize: watchFace!.watchLayout.watchSize) - watchFace!.updateSize(with: CGRect(x: (screen.width - newSize.width) / 2.0, y: (screen.height - newSize.height) / 2.0, - width: newSize.width, height: newSize.height)) - } - - override func viewDidLoad() { - super.viewDidLoad() - - resize() - watchFace.setAutoRefresh() - - let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressed)) - longPressGesture.minimumPressDuration = 0.5 - longPressGesture.delegate = self - view.addGestureRecognizer(longPressGesture) - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped(_:))) - watchFace!.addGestureRecognizer(tapRecognizer) - } - - override func viewDidAppear(_ animated: Bool) { - let launchedBefore = UserDefaults(suiteName: DataContainer.groupId)?.bool(forKey: "ChineseTimeLaunchedBefore") ?? false - if !launchedBefore { - let storyBoard = UIStoryboard(name: "Main", bundle: nil) - let welcome = storyBoard.instantiateViewController(withIdentifier: "WelcomeView") as! WelcomeViewController - present(welcome, animated: true) - } - _ = WatchConnectivityManager.shared - } - - @objc func tapped(_ gesture: UITapGestureRecognizer) { - guard gesture.state == .ended else { return } - let point = gesture.location(in: watchFace) - let shortEdge = min(watchFace.bounds.width, watchFace.bounds.height) - var entities = [EntityNote]() - for entity in watchFace.entityNotes { - let diff = point - entity.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < GraphicArtifects.markRadius * 5 * shortEdge { - entities.append(entity) - } - } - if entities.count > 0 { - let newPoint = watchFace.convert(point, to: view) - let boundingRect = view.bounds.insetBy(dx: WatchFaceView.frameOffset, dy: WatchFaceView.frameOffset) - let tooltip = NoteView(center: newPoint, bounds: boundingRect, entities: entities) - tooltipView?.removeFromSuperview() - if let tooltip = tooltip { - view.addSubview(tooltip) - } - tooltipView = tooltip - } - } - - @objc func longPressed(gestureRecognizer: UILongPressGestureRecognizer) { - switch gestureRecognizer.state { - case .began: - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - case .ended: - let storyBoard = UIStoryboard(name: "Main", bundle: nil) - let settingsViewController = storyBoard.instantiateViewController(withIdentifier: "Settings") as! UINavigationController - present(settingsViewController, animated: true, completion: nil) - default: - break - } - } - - override func viewWillLayoutSubviews() { - super.viewDidLayoutSubviews() - resize() - } -} - -final class WelcomeViewController: UIViewController { - @IBOutlet var appName: UILabel! - @IBOutlet var height: NSLayoutConstraint! - @IBOutlet var watchFaceTop: NSLayoutConstraint! - @IBOutlet var contentTop: NSLayoutConstraint! - @IBOutlet var text1: UITextView! - @IBOutlet var text2: UITextView! - @IBOutlet var button: UIButton! - - @IBAction func close(_ sender: UIButton) { - if let userDefaults = UserDefaults(suiteName: DataContainer.groupId) { - userDefaults.set(true, forKey: "ChineseTimeLaunchedBefore") - userDefaults.synchronize() - } - dismiss(animated: true) - } - - override func viewDidLoad() { - super.viewDidLoad() - appName.font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize, weight: .black) - button.configuration?.cornerStyle = .large - contentTop.constant = max(0.25 * view.bounds.height - 100, 20) - watchFaceTop.constant = max(0.12 * view.bounds.height - 40, 10) - height.constant = 510.0 + contentTop.constant + watchFaceTop.constant - 60 - text1.text = NSLocalizedString("輪試設計介紹", comment: "Details about Ring Design") - text2.text = NSLocalizedString("設置介紹", comment: "Details about Settings") - } -} - -final class TableCell: UITableViewCell { - static let identifier = "UITableViewCell" - var title: String? - var pushView: (() -> Void)? - var desp1: String? - var desp2: String? - var elements = UIView() - var segment: UISegmentedControl? - - override func layoutSubviews() { - super.layoutSubviews() - - elements.removeFromSuperview() - segment?.removeFromSuperview() - elements = UIView() - - let labelSize: CGSize - if desp1 == nil && desp2 == nil && segment == nil { - labelSize = CGSize(width: 200, height: 21) - } else { - labelSize = CGSize(width: 110, height: 21) - } - - let label = UILabel() - label.frame = CGRect(x: 15, y: (bounds.height - labelSize.height) / 2, width: labelSize.width, height: labelSize.height) - label.text = title - elements.addSubview(label) - - if pushView != nil { - let arrow = UIImageView(image: UIImage(systemName: "chevron.forward")!) - let arrowSize = CGSize(width: 9, height: 12) - arrow.frame = CGRect(x: bounds.width - arrowSize.width - 15, y: (bounds.height - arrowSize.height) / 2, width: arrowSize.width, height: arrowSize.height) - arrow.tintColor = .systemGray - elements.addSubview(arrow) - - if let desp1 = desp1, let desp2 = desp2 { - let label1 = UILabel() - label1.text = desp1 - label1.textColor = .secondaryLabel - label1.frame = CGRect(x: CGRectGetMaxX(label.frame) + 15, y: bounds.height / 2 - labelSize.height - 1, width: CGRectGetMinX(arrow.frame) - CGRectGetMaxX(label.frame) - 30, height: labelSize.height) - elements.addSubview(label1) - let label2 = UILabel() - label2.text = desp2 - label2.textColor = .secondaryLabel - label2.frame = CGRect(x: CGRectGetMaxX(label.frame) + 15, y: bounds.height / 2 + 1, width: CGRectGetMinX(arrow.frame) - CGRectGetMaxX(label.frame) - 30, height: labelSize.height) - elements.addSubview(label2) - } - } else if segment != nil { - segment!.frame = CGRect(x: CGRectGetMaxX(label.frame) + 15, y: (bounds.height - labelSize.height * 1.6) / 2, width: bounds.width - CGRectGetMaxX(label.frame) - 30, height: labelSize.height * 1.6) - addSubview(segment!) - } - - addSubview(elements) - } - - override func prepareForReuse() { - super.prepareForReuse() - elements.removeFromSuperview() - segment?.removeFromSuperview() - elements = UIView() - title = nil - pushView = nil - desp1 = nil - desp2 = nil - segment = nil - } -} - -final class SettingsViewController: UITableViewController { - struct ButtonOption { - let title: String - let color: UIColor - let action: () -> Void - } - - struct DuelOption { - let title: String - let segment: UISegmentedControl - } - - struct DetailOption { - let title: String - let action: (() -> Void)? - let desp1: String? - let desp2: String? - } - - enum SettingsOption { - case detail(model: DetailOption) - case dual(model: DuelOption) - } - - struct Section { - let title: String - let options: [SettingsOption] - } - - var models = [Section]() - - func createNextView(name: String) -> (() -> Void) { - func openView() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let nextView = storyboard.instantiateViewController(withIdentifier: name) - navigationController?.pushViewController(nextView, animated: true) - } - return openView - } - - func reload() { - fillData() - tableView.reloadData() - } - - func fillData() { - let time = WatchFaceView.currentInstance?.displayTime ?? Date() - let timezone = WatchFaceView.currentInstance?.timezone ?? Calendar.current.timeZone - let locationString = WatchFaceView.currentInstance?.location.map { coordinateDesp(coordinate: $0) } - - let globalMonthSegment = UISegmentedControl(items: [NSLocalizedString("精確至時刻", comment: "Leap month setting: precise"), NSLocalizedString("精確至日", comment: "Leap month setting: daily precision")]) - globalMonthSegment.selectedSegmentIndex = ChineseCalendar.globalMonth ? 0 : 1 - globalMonthSegment.addTarget(self, action: #selector(globalMonthToggled(segment:)), for: .allEvents) - - let apparentTimeSegment = UISegmentedControl(items: [NSLocalizedString("真太陽時", comment: "Time setting: apparent solar time"), NSLocalizedString("標準時", comment: "Time setting: mean solar time")]) - apparentTimeSegment.isEnabled = WatchFaceView.currentInstance?.location != nil - apparentTimeSegment.selectedSegmentIndex = WatchFaceView.currentInstance?.location == nil ? 1 : (ChineseCalendar.apparentTime ? 0 : 1) - apparentTimeSegment.addTarget(self, action: #selector(apparentTimeToggled(segment:)), for: .allEvents) - - models = [ - Section(title: NSLocalizedString("數據", comment: "Data Source"), options: [ - .dual(model: DuelOption(title: NSLocalizedString("置閏法", comment: "Leap month setting"), segment: globalMonthSegment)), - .dual(model: DuelOption(title: NSLocalizedString("時間", comment: "Time setting"), segment: apparentTimeSegment)), - .detail(model: DetailOption(title: NSLocalizedString("顯示時間", comment: "Display time"), action: createNextView(name: "DateTime"), - desp1: time.formatted(date: .numeric, time: .shortened), desp2: timezone.localizedName(for: .generic, locale: Locale.current))), - .detail(model: DetailOption(title: NSLocalizedString("經緯度", comment: "Location"), action: createNextView(name: "Location"), desp1: locationString?.0, desp2: locationString?.1)) - ]), - Section(title: NSLocalizedString("樣式", comment: "Styles"), options: [ - .detail(model: DetailOption(title: NSLocalizedString("圈色", comment: "Circle colors"), action: createNextView(name: "CircleColors"), desp1: nil, desp2: nil)), - .detail(model: DetailOption(title: NSLocalizedString("塊標色", comment: "Mark colors"), action: createNextView(name: "MarkColors"), desp1: nil, desp2: nil)), - .detail(model: DetailOption(title: NSLocalizedString("佈局", comment: "Layout parameters"), action: createNextView(name: "Layouts"), desp1: nil, desp2: nil)) - ]), - Section(title: NSLocalizedString("其它", comment: "Action"), options: [ - .detail(model: DetailOption(title: NSLocalizedString("主題庫", comment: "manage saved layouts"), action: createNextView(name: "ThemeList"), desp1: nil, desp2: nil)), - .detail(model: DetailOption(title: NSLocalizedString("注釋", comment: "Help Doc"), action: createNextView(name: "HelpView"), desp1: nil, desp2: nil)) - ]) - ] - } - - override func viewDidLoad() { - super.viewDidLoad() - title = NSLocalizedString("設置", comment: "Settings View") - navigationItem.setRightBarButton(UIBarButtonItem(title: NSLocalizedString("畢", comment: "Close settings panel"), style: .done, target: navigationController, action: #selector(UINavigationController.closeSetting(_:))), animated: false) - tableView = UITableView(frame: tableView.frame, style: .insetGrouped) - tableView.register(TableCell.self, forCellReuseIdentifier: TableCell.identifier) - fillData() - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return models.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return models[section].options.count - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return models[section].title - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let data = models[indexPath.section].options[indexPath.row] - switch data { - case .detail(model: let model): - if model.desp1 != nil && model.desp2 != nil { - return 60 - } else { - return 44 - } - default: - return 44 - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let option = models[indexPath.section].options[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: TableCell.identifier, for: indexPath) as! TableCell - switch option { - case .detail(model: let model): - cell.title = model.title - cell.desp1 = model.desp1 - cell.desp2 = model.desp2 - cell.pushView = model.action - case .dual(model: let model): - cell.title = model.title - cell.segment = model.segment - } - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let cell = (tableView.cellForRow(at: indexPath) as! TableCell) - if let action = cell.pushView { - action() - } - } - - @objc func globalMonthToggled(segment: UISegmentedControl) { - if segment.selectedSegmentIndex == 0 { - ChineseCalendar.globalMonth = true - } else if segment.selectedSegmentIndex == 1 { - ChineseCalendar.globalMonth = false - } - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - - @objc func apparentTimeToggled(segment: UISegmentedControl) { - if segment.selectedSegmentIndex == 0 { - ChineseCalendar.apparentTime = true - if WatchFaceView.currentInstance?.location == nil { - segment.selectedSegmentIndex = 1 - } - } else if segment.selectedSegmentIndex == 1 { - ChineseCalendar.apparentTime = false - } - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } -} - -final class LocationView: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { - static var currentInstance: LocationView? - - @IBOutlet var viewHeight: NSLayoutConstraint! - @IBOutlet var longitudePicker: UIPickerView! - @IBOutlet var latitudePicker: UIPickerView! - @IBOutlet var display: UITextField! - @IBOutlet var toggleView: UIView! - @IBOutlet var displayView: UIView! - @IBOutlet var pickerView: UIView! - @IBOutlet var locationOptions: UISegmentedControl! - @IBOutlet var locationTitle: UILabel! - @IBOutlet var currentLocationSwitch: UISwitch! - - var longitude: [Int] = [0, 0, 0, 0] - var latitude: [Int] = [0, 0, 0, 0] - - func makeSelection(value: Double, picker: UIPickerView) { - var values = [0, 0, 0, 0] - values[3] = value >= 0 ? 0 : 1 - picker.selectRow(values[3], inComponent: 3, animated: true) - let tempValue = Int(round(abs(value) * 3600)) - values[0] = tempValue / 3600 - if picker === longitudePicker { - picker.selectRow(values[0] + 180 * 10, inComponent: 0, animated: true) - } else if picker == latitudePicker { - picker.selectRow(values[0] + 90 * 10, inComponent: 0, animated: true) - } - values[1] = (tempValue % 3600) / 60 - picker.selectRow(values[1] + 60 * 10, inComponent: 1, animated: true) - values[2] = tempValue % 60 - picker.selectRow(values[2] + 60 * 10, inComponent: 2, animated: true) - if picker === longitudePicker { - longitude = values - } else if picker === latitudePicker { - latitude = values - } - } - - func chooseLocationOption(of choice: Int) { // Will not trigger requestLocation - if choice == 0 { - LocationManager.shared.enabled = false - locationOptions.selectedSegmentIndex = 0 - pickerView.isHidden = false - displayView.isHidden = true - viewHeight.constant = CGRectGetMaxY(pickerView.frame) + 20 - if let location = WatchLayout.shared.location ?? LocationManager.shared.location { - makeSelection(value: location.y, picker: longitudePicker) - makeSelection(value: location.x, picker: latitudePicker) - } else { - makeSelection(value: 0, picker: longitudePicker) - makeSelection(value: 0, picker: latitudePicker) - } - } else if choice == 1 { - LocationManager.shared.enabled = true - locationOptions.selectedSegmentIndex = 1 - pickerView.isHidden = true - displayView.isHidden = false - viewHeight.constant = CGRectGetMaxY(displayView.frame) + 20 - if let location = LocationManager.shared.location { - let locationString = coordinateDesp(coordinate: location) - display.text = "\(locationString.0), \(locationString.1)" - } else { - display.text = NSLocalizedString("虚無", comment: "Location fails to load") - } - } - } - - func fillData() { - if WatchFaceView.currentInstance?.location != nil { - currentLocationSwitch.isOn = true - locationTitle.isHidden = false - locationOptions.isEnabled = true - if LocationManager.shared.enabled { - chooseLocationOption(of: 1) - LocationManager.shared.requestLocation { location in - if location != nil { - self.chooseLocationOption(of: 1) - } else { - self.chooseLocationOption(of: 0) - } - } - } else { - chooseLocationOption(of: 0) - } - } else { - currentLocationSwitch.isOn = false - locationTitle.isHidden = true - pickerView.isHidden = true - displayView.isHidden = true - locationOptions.isEnabled = false - } - } - - override func viewDidLoad() { - super.viewDidLoad() - title = NSLocalizedString("經緯度", comment: "Location View") - navigationItem.largeTitleDisplayMode = .never - pickerView.layer.cornerRadius = 10 - displayView.layer.cornerRadius = 10 - toggleView.layer.cornerRadius = 10 - longitudePicker.delegate = self - longitudePicker.dataSource = self - latitudePicker.delegate = self - latitudePicker.dataSource = self - Self.currentInstance = self - navigationItem.setRightBarButton(UIBarButtonItem(title: NSLocalizedString("畢", comment: "Close settings panel"), style: .done, target: navigationController, action: #selector(UINavigationController.closeSetting(_:))), animated: false) - fillData() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - Self.currentInstance = nil - } - - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 4 - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - if pickerView === longitudePicker { - let numbers = [0: 180 * 20, 1: 60 * 20, 2: 60 * 20, 3: 2] - return numbers[component]! - } else if pickerView === latitudePicker { - let numbers = [0: 90 * 20, 1: 60 * 20, 2: 60 * 20, 3: 2] - return numbers[component]! - } else { - return 0 - } - } - - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - switch component { - case 0, 1, 2: - if pickerView === longitudePicker { - if component == 0 { - return "\(row % 180)" - } else { - return "\(row % 60)" - } - } else if pickerView === latitudePicker { - if component == 0 { - return "\(row % 90)" - } else { - return "\(row % 60)" - } - } else { - return nil - } - case 3: - if pickerView === longitudePicker { - return row == 0 ? "E" : "W" - } else if pickerView === latitudePicker { - return row == 0 ? "N" : "S" - } else { - return nil - } - default: - return nil - } - } - - func readCoordinate() -> CGPoint { - var longitudeValue = Double(longitude[0]) - longitudeValue += Double(longitude[1]) / 60 - longitudeValue += Double(longitude[2]) / 3600 - longitudeValue *= longitude[3] == 0 ? 1.0 : -1.0 - var latitudeValue = Double(latitude[0]) - latitudeValue += Double(latitude[1]) / 60 - latitudeValue += Double(latitude[2]) / 3600 - latitudeValue *= latitude[3] == 0 ? 1.0 : -1.0 - return CGPoint(x: latitudeValue, y: longitudeValue) - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - LocationManager.shared.location = nil - if pickerView === longitudePicker { - switch component { - case 0: - longitude[component] = row % 180 - case 1, 2: - longitude[component] = row % 60 - case 3: - longitude[component] = row - default: - break - } - } else if pickerView === latitudePicker { - switch component { - case 0: - latitude[component] = row % 90 - case 1, 2: - latitude[component] = row % 60 - case 3: - latitude[component] = row - default: - break - } - } - let coordinate = readCoordinate() - WatchLayout.shared.location = coordinate - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } - - @IBAction func currentLocationToggled(_ sender: UISwitch) { - if currentLocationSwitch.isOn { - locationOptions.isEnabled = true - locationTitle.isHidden = false - if LocationManager.shared.enabled { - LocationManager.shared.requestLocation { location in - if location != nil { - self.chooseLocationOption(of: 1) - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (self.navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } else { - self.chooseLocationOption(of: 0) - } - } - } else { - chooseLocationOption(of: 0) - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } else { - locationTitle.isHidden = true - pickerView.isHidden = true - displayView.isHidden = true - locationOptions.isEnabled = false - viewHeight.constant = CGRectGetMaxY(toggleView.frame) + 20 - LocationManager.shared.location = nil - WatchLayout.shared.location = nil - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } - } - - func presentLocationUnavailable() { - let alertController = UIAlertController(title: NSLocalizedString("怪哉", comment: "Location not enabled but tried to locate title"), message: NSLocalizedString("蓋因定位未開啓", comment: "Location not enabled but tried to locate message"), preferredStyle: .alert) - let cancelAction = UIAlertAction(title: NSLocalizedString("作罷", comment: "Ok"), style: .default) - - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - - @IBAction func locationOptionToggled(_ sender: UISegmentedControl) { - chooseLocationOption(of: sender.selectedSegmentIndex) - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - if sender.selectedSegmentIndex == 1 { - LocationManager.shared.enabled = true - LocationManager.shared.requestLocation { location in - if location != nil { - self.chooseLocationOption(of: 1) - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (self.navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } else { - self.chooseLocationOption(of: 0) - self.presentLocationUnavailable() - } - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } else if sender.selectedSegmentIndex == 0 { - LocationManager.shared.enabled = false - LocationManager.shared.location = nil - let coordinate = readCoordinate() - WatchLayout.shared.location = coordinate - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } - } -} - -final class DateTimeView: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { - @IBOutlet var datetimePicker: UIDatePicker! - @IBOutlet var timezonePicker: UIPickerView! - @IBOutlet var currentTime: UISwitch! - @IBOutlet var contentView: UIView! - - var panelTimezone = Calendar.current.timeZone - var timeZones = DataTree(name: "Root") - var timer: Timer? - - func populateTimezones() { - let allTimezones = TimeZone.knownTimeZoneIdentifiers - for timezone in allTimezones { - let components = timezone.split(separator: "/") - var currentNode: DataTree? = timeZones - for component in components { - currentNode = currentNode?.add(element: String(component)) - } - } - } - - func selectTimezone(timezone: TimeZone) { - let components = timezone.identifier.split(separator: "/") - var currentNode: DataTree? = timeZones - for i in 0.. Int { - return timeZones.maxLevel - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - var currentNode: DataTree? = timeZones - for i in 0.. String? { - var currentNode: DataTree? = timeZones - for i in 0.. UIView { - let label: UILabel - - if let view = view { - label = view as! UILabel - } else { - label = UILabel() - label.font = UIFont.systemFont(ofSize: UIFont.labelFontSize) - label.lineBreakMode = .byTruncatingTail - label.numberOfLines = 1 - label.adjustsFontSizeToFitWidth = true - label.textAlignment = .center - } - - label.text = self.pickerView(pickerView, titleForRow: row, forComponent: component) - return label - } - - @IBAction func dateChanged(_ sender: UIDatePicker) { - currentTime.isOn = false - let selectedDate = datetimePicker.date.convertToTimeZone(initTimeZone: panelTimezone, timeZone: Calendar.current.timeZone) - let secondDiff = Calendar.current.component(.second, from: selectedDate) - WatchFaceView.currentInstance?.displayTime = selectedDate.advanced(by: -Double(secondDiff)) - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } - - @IBAction func currentDateToggled(_ sender: UISwitch) { - if currentTime.isOn { - WatchFaceView.currentInstance?.displayTime = nil - WatchFaceView.currentInstance?.timezone = Calendar.current.timeZone - datetimePicker.date = Date() - selectTimezone(timezone: Calendar.current.timeZone) - } else { - WatchFaceView.currentInstance?.displayTime = datetimePicker.date - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } -} - -final class ColorWell: UIColorWell { - var index: Int! - - @objc func dragged(_ sender: UIPanGestureRecognizer) { - guard let slider = superview as? GradientSlider else { return } - let translation = sender.translation(in: slider) - center = CGPoint(x: center.x + translation.x, y: center.y + translation.y) - sender.setTranslation(CGPoint.zero, in: slider) - if sender.state == .ended { - if slider.bounds.contains(center) || slider.controls.count <= 2 { - frame = CGRect(x: frame.origin.x, y: (slider.bounds.height - frame.height) / 2, width: frame.width, height: frame.height) - slider.values[index] = (center.x - slider.bounds.origin.x) / (slider.bounds.width - slider.controlRadius * 2) - } else { - removeFromSuperview() - slider.removeControl(at: index) - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - } - slider.updateGradient() - if let action = slider.action { - action() - } - } - } - - @objc func colorWellChanged(_ sender: Any) { - guard let slider = superview as? GradientSlider else { return } - if let color = selectedColor { - slider.colors[index] = color - slider.updateGradient() - if let action = slider.action { - action() - } - } - } -} - -final class GradientSlider: UIControl, UIGestureRecognizerDelegate { - let minimumValue: CGFloat = 0 - let maximumValue: CGFloat = 1 - var values: [CGFloat] = [0, 1] - var colors: [UIColor] = [.black, .white] - internal var controls = [ColorWell]() - var action: (() -> Void)? - - var isLoop = false - private let trackLayer = CAGradientLayer() - internal var controlRadius: CGFloat = 0 - - var gradient: WatchLayout.Gradient { - get { - return WatchLayout.Gradient(locations: values, colors: colors.map { $0.cgColor }, loop: isLoop) - } set { - if newValue.isLoop { - values = newValue.locations.dropLast() - colors = newValue.colors.dropLast().map { UIColor(cgColor: $0) } - } else { - values = newValue.locations - colors = newValue.colors.map { UIColor(cgColor: $0) } - } - isLoop = newValue.isLoop - updateLayerFrames() - initializeControls() - updateGradient() - } - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - backgroundColor = .clear - layer.addSublayer(trackLayer) - updateLayerFrames() - initializeControls() - updateGradient() - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - if let location = touches.first?.location(in: self) { - let ratio = (location.x - bounds.origin.x - controlRadius) / (bounds.width - controlRadius * 2) - let color = UIColor(cgColor: gradient.interpolate(at: ratio)) - addControl(at: ratio, color: color) - values.append(ratio) - colors.append(color) - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - updateGradient() - if let action = action { - action() - } - } - super.touchesBegan(touches, with: event) - } - - override var frame: CGRect { - didSet { - updateLayerFrames() - changeChontrols() - } - } - - private func addControl(at value: CGFloat, color: UIColor) { - let control = ColorWell() - control.frame = CGRect(origin: thumbOriginForValue(value), size: CGSize(width: controlRadius * 2, height: controlRadius * 2)) - control.selectedColor = color - let panGesture = UIPanGestureRecognizer(target: control, action: #selector(ColorWell.dragged(_:))) - control.isUserInteractionEnabled = true - control.addGestureRecognizer(panGesture) - control.addTarget(control, action: #selector(ColorWell.colorWellChanged(_:)), for: .allEvents) - controls.append(control) - control.index = controls.count - 1 - addSubview(control) - } - - private func initializeControls() { - for control in controls.reversed() { - control.removeFromSuperview() - } - controls = [] - for i in 0.. CGFloat { - return trackLayer.frame.width * value - } - - private func thumbOriginForValue(_ value: CGFloat) -> CGPoint { - let x = positionForValue(value) - controlRadius - return CGPoint(x: trackLayer.frame.minX + x, y: bounds.height / 2 - controlRadius) - } -} - -final class CircleColorView: UIViewController { - @IBOutlet var yearColor: GradientSlider! - @IBOutlet var monthColor: GradientSlider! - @IBOutlet var dayColor: GradientSlider! - @IBOutlet var centerTextColor: GradientSlider! - @IBOutlet var yearColorLoop: UISwitch! - @IBOutlet var monthColorLoop: UISwitch! - @IBOutlet var dayColorLoop: UISwitch! - @IBOutlet var circleTransparancy: UISlider! - @IBOutlet var majorTickTransparancy: UISlider! - @IBOutlet var minorTickTransparancy: UISlider! - @IBOutlet var circleTransparancyReading: UILabel! - @IBOutlet var majorTickTransparancyReading: UILabel! - @IBOutlet var minorTickTransparancyReading: UILabel! - @IBOutlet var firstSection: UIView! - @IBOutlet var secondSection: UIView! - @IBOutlet var thirdSection: UIView! - - @IBOutlet var majorTickColor: UIColorWell! - @IBOutlet var majorTickColorDark: UIColorWell! - @IBOutlet var minorTickColor: UIColorWell! - @IBOutlet var minorTickColorDark: UIColorWell! - @IBOutlet var oddSolarTermColor: UIColorWell! - @IBOutlet var oddSolarTermColorDark: UIColorWell! - @IBOutlet var evenSolarTermColor: UIColorWell! - @IBOutlet var evenSolarTermColorDark: UIColorWell! - @IBOutlet var textColor: UIColorWell! - @IBOutlet var textColorDark: UIColorWell! - @IBOutlet var coreColor: UIColorWell! - @IBOutlet var coreColorDark: UIColorWell! - - func fillData() { - let layout = WatchLayout.shared - yearColor.gradient = layout.firstRing - yearColorLoop.isOn = layout.firstRing.isLoop - monthColor.gradient = layout.secondRing - monthColorLoop.isOn = layout.secondRing.isLoop - dayColor.gradient = layout.thirdRing - dayColorLoop.isOn = layout.thirdRing.isLoop - centerTextColor.gradient = layout.centerFontColor - - circleTransparancy.value = Float(layout.shadeAlpha) - circleTransparancyReading.text = String(format: "%.2f", layout.shadeAlpha) - majorTickTransparancy.value = Float(layout.majorTickAlpha) - majorTickTransparancyReading.text = String(format: "%.2f", layout.majorTickAlpha) - minorTickTransparancy.value = Float(layout.minorTickAlpha) - minorTickTransparancyReading.text = String(format: "%.2f", layout.minorTickAlpha) - - majorTickColor.selectedColor = UIColor(cgColor: layout.majorTickColor) - majorTickColorDark.selectedColor = UIColor(cgColor: layout.majorTickColorDark) - minorTickColor.selectedColor = UIColor(cgColor: layout.minorTickColor) - minorTickColorDark.selectedColor = UIColor(cgColor: layout.minorTickColorDark) - oddSolarTermColor.selectedColor = UIColor(cgColor: layout.oddSolarTermTickColor) - oddSolarTermColorDark.selectedColor = UIColor(cgColor: layout.oddSolarTermTickColorDark) - evenSolarTermColor.selectedColor = UIColor(cgColor: layout.evenSolarTermTickColor) - evenSolarTermColorDark.selectedColor = UIColor(cgColor: layout.evenSolarTermTickColorDark) - textColor.selectedColor = UIColor(cgColor: layout.fontColor) - textColorDark.selectedColor = UIColor(cgColor: layout.fontColorDark) - coreColor.selectedColor = UIColor(cgColor: layout.innerColor) - coreColorDark.selectedColor = UIColor(cgColor: layout.innerColorDark) - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationItem.setRightBarButton(UIBarButtonItem(title: NSLocalizedString("畢", comment: "Close settings panel"), style: .done, target: navigationController, action: #selector(UINavigationController.closeSetting(_:))), animated: false) - title = NSLocalizedString("圈色", comment: "Circle Color View") - navigationItem.largeTitleDisplayMode = .never - firstSection.layer.cornerRadius = 10 - secondSection.layer.cornerRadius = 10 - thirdSection.layer.cornerRadius = 10 - yearColor.action = { - WatchLayout.shared.firstRing = self.yearColor.gradient - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - monthColor.action = { - WatchLayout.shared.secondRing = self.monthColor.gradient - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - dayColor.action = { - WatchLayout.shared.thirdRing = self.dayColor.gradient - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - centerTextColor.action = { - WatchLayout.shared.centerFontColor = self.centerTextColor.gradient - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - fillData() - - majorTickColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - majorTickColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - minorTickColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - minorTickColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - oddSolarTermColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - oddSolarTermColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - evenSolarTermColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - evenSolarTermColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - textColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - textColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - coreColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - coreColorDark.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - } - - @IBAction func loopToggled(_ sender: UISwitch) { - let watchLayout = WatchLayout.shared - if sender === yearColorLoop { - yearColor.isLoop = sender.isOn - yearColor.updateGradient() - watchLayout.firstRing = yearColor.gradient - } else if sender === monthColorLoop { - monthColor.isLoop = sender.isOn - monthColor.updateGradient() - watchLayout.secondRing = monthColor.gradient - } else if sender === dayColorLoop { - dayColor.isLoop = sender.isOn - dayColor.updateGradient() - watchLayout.thirdRing = dayColor.gradient - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - - @IBAction func transparencyChanged(_ sender: UISlider) { - let watchLayout = WatchLayout.shared - if sender === circleTransparancy { - circleTransparancyReading.text = String(format: "%.2f", sender.value) - watchLayout.shadeAlpha = CGFloat(circleTransparancy.value) - } else if sender === majorTickTransparancy { - majorTickTransparancyReading.text = String(format: "%.2f", sender.value) - watchLayout.majorTickAlpha = CGFloat(majorTickTransparancy.value) - } else if sender === minorTickTransparancy { - minorTickTransparancyReading.text = String(format: "%.2f", sender.value) - watchLayout.minorTickAlpha = CGFloat(minorTickTransparancy.value) - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } - - @objc func colorChanged(_ sender: UIColorWell) { - let watchLayout = WatchLayout.shared - if sender === majorTickColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.majorTickColor = color - } - } else if sender === majorTickColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.majorTickColorDark = color - } - } else if sender === majorTickColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.majorTickColor = color - } - } else if sender === minorTickColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.minorTickColorDark = color - } - } else if sender === oddSolarTermColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.oddSolarTermTickColor = color - } - } else if sender === oddSolarTermColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.oddSolarTermTickColorDark = color - } - } else if sender === evenSolarTermColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.evenSolarTermTickColor = color - } - } else if sender === evenSolarTermColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.evenSolarTermTickColorDark = color - } - } else if sender === textColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.fontColor = color - } - } else if sender === textColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.fontColorDark = color - } - } else if sender === coreColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.innerColor = color - } - } else if sender === coreColorDark { - if let color = sender.selectedColor?.cgColor { - watchLayout.innerColorDark = color - } - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } -} - -final class MarkColorView: UIViewController { - @IBOutlet var firstSection: UIView! - @IBOutlet var secondSection: UIView! - @IBOutlet var thirdSection: UIView! - @IBOutlet var fourthSection: UIView! - - @IBOutlet var mercuryColor: UIColorWell! - @IBOutlet var venusColor: UIColorWell! - @IBOutlet var marsColor: UIColorWell! - @IBOutlet var jupiterColor: UIColorWell! - @IBOutlet var saturnColor: UIColorWell! - @IBOutlet var moonColor: UIColorWell! - - @IBOutlet var newmoonMarkColor: UIColorWell! - @IBOutlet var fullmoonMarkColor: UIColorWell! - @IBOutlet var oddSolarTermMarkColor: UIColorWell! - @IBOutlet var evenSolarTermMarkColor: UIColorWell! - - @IBOutlet var sunriseMarkColor: UIColorWell! - @IBOutlet var sunsetMarkColor: UIColorWell! - @IBOutlet var noonMarkColor: UIColorWell! - @IBOutlet var midnightMarkColor: UIColorWell! - - @IBOutlet var moonriseMarkColor: UIColorWell! - @IBOutlet var moonsetMarkColor: UIColorWell! - @IBOutlet var moonnoonMarkColor: UIColorWell! - - func fillData() { - let layout = WatchLayout.shared - mercuryColor.selectedColor = UIColor(cgColor: layout.planetIndicator[0]) - venusColor.selectedColor = UIColor(cgColor: layout.planetIndicator[1]) - marsColor.selectedColor = UIColor(cgColor: layout.planetIndicator[2]) - jupiterColor.selectedColor = UIColor(cgColor: layout.planetIndicator[3]) - saturnColor.selectedColor = UIColor(cgColor: layout.planetIndicator[4]) - moonColor.selectedColor = UIColor(cgColor: layout.planetIndicator[5]) - - newmoonMarkColor.selectedColor = UIColor(cgColor: layout.eclipseIndicator) - fullmoonMarkColor.selectedColor = UIColor(cgColor: layout.fullmoonIndicator) - oddSolarTermMarkColor.selectedColor = UIColor(cgColor: layout.oddStermIndicator) - evenSolarTermMarkColor.selectedColor = UIColor(cgColor: layout.evenStermIndicator) - - sunriseMarkColor.selectedColor = UIColor(cgColor: layout.sunPositionIndicator[1]) - sunsetMarkColor.selectedColor = UIColor(cgColor: layout.sunPositionIndicator[3]) - noonMarkColor.selectedColor = UIColor(cgColor: layout.sunPositionIndicator[2]) - midnightMarkColor.selectedColor = UIColor(cgColor: layout.sunPositionIndicator[0]) - - moonriseMarkColor.selectedColor = UIColor(cgColor: layout.moonPositionIndicator[0]) - moonsetMarkColor.selectedColor = UIColor(cgColor: layout.moonPositionIndicator[2]) - moonnoonMarkColor.selectedColor = UIColor(cgColor: layout.moonPositionIndicator[1]) - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationItem.setRightBarButton(UIBarButtonItem(title: NSLocalizedString("畢", comment: "Close settings panel"), style: .done, target: navigationController, action: #selector(UINavigationController.closeSetting(_:))), animated: false) - title = NSLocalizedString("塊標色", comment: "Mark Color View") - navigationItem.largeTitleDisplayMode = .never - firstSection.layer.cornerRadius = 10 - secondSection.layer.cornerRadius = 10 - thirdSection.layer.cornerRadius = 10 - fourthSection.layer.cornerRadius = 10 - fillData() - - mercuryColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - venusColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - marsColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - jupiterColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - saturnColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - moonColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - newmoonMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - fullmoonMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - oddSolarTermMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - evenSolarTermMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - sunriseMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - sunsetMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - noonMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - midnightMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - moonriseMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - moonsetMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - moonnoonMarkColor.addTarget(self, action: #selector(colorChanged(_:)), for: .valueChanged) - } - - @objc func colorChanged(_ sender: UIColorWell) { - let watchLayout = WatchLayout.shared - if sender === mercuryColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[0] = color - } - } else if sender === venusColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[1] = color - } - } else if sender === marsColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[2] = color - } - } else if sender === jupiterColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[3] = color - } - } else if sender === saturnColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[4] = color - } - } else if sender === moonColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.planetIndicator[5] = color - } - } else if sender === newmoonMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.eclipseIndicator = color - } - } else if sender === fullmoonMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.fullmoonIndicator = color - } - } else if sender === oddSolarTermMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.oddStermIndicator = color - } - } else if sender === evenSolarTermMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.evenStermIndicator = color - } - } else if sender === sunriseMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.sunPositionIndicator[1] = color - } - } else if sender === sunsetMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.sunPositionIndicator[3] = color - } - } else if sender === noonMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.sunPositionIndicator[2] = color - } - } else if sender === midnightMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.sunPositionIndicator[0] = color - } - } else if sender === moonriseMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.moonPositionIndicator[0] = color - } - } else if sender === moonsetMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.moonPositionIndicator[2] = color - } - } else if sender === moonnoonMarkColor { - if let color = sender.selectedColor?.cgColor { - watchLayout.moonPositionIndicator[1] = color - } - } - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } -} - -final class LayoutsView: UIViewController { - @IBOutlet var contentView: UIView! - @IBOutlet var widthField: UITextField! - @IBOutlet var heightField: UITextField! - @IBOutlet var roundedCornerField: UITextField! - @IBOutlet var largeTextShiftField: UITextField! - @IBOutlet var largeTextHShiftField: UITextField! - @IBOutlet var textVerticalShiftField: UITextField! - @IBOutlet var textHorizontalShiftField: UITextField! - - func fillData() { - let layout = WatchLayout.shared - widthField.text = String(format: "%.0f", layout.watchSize.width) - heightField.text = String(format: "%.0f", layout.watchSize.height) - roundedCornerField.text = String(format: "%.2f", layout.cornerRadiusRatio) - largeTextShiftField.text = String(format: "%.2f", layout.centerTextOffset) - largeTextHShiftField.text = String(format: "%.2f", layout.centerTextHOffset) - textVerticalShiftField.text = String(format: "%.2f", layout.verticalTextOffset) - textHorizontalShiftField.text = String(format: "%.2f", layout.horizontalTextOffset) - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationItem.setRightBarButton(UIBarButtonItem(title: NSLocalizedString("畢", comment: "Close settings panel"), style: .done, target: navigationController, action: #selector(UINavigationController.closeSetting(_:))), animated: false) - title = NSLocalizedString("佈局", comment: "Layout Parameter View") - navigationItem.largeTitleDisplayMode = .never - contentView.layer.cornerRadius = 10 - fillData() - } - - @IBAction func widthChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.watchSize.width = value - (WatchFaceView.currentInstance?.window?.rootViewController as? ViewController)?.resize() - } else { - sender.text = nil - } - } - - @IBAction func heightChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.watchSize.height = value - (WatchFaceView.currentInstance?.window?.rootViewController as? ViewController)?.resize() - } else { - sender.text = nil - } - } - - @IBAction func radiusChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.cornerRadiusRatio = value - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } else { - sender.text = nil - } - } - - @IBAction func largeTextShiftChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.centerTextOffset = value - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } else { - sender.text = nil - } - } - - @IBAction func largeTextHShiftChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.centerTextHOffset = value - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } else { - sender.text = nil - } - } - - @IBAction func textVerticalShiftChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.verticalTextOffset = value - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } else { - sender.text = nil - } - } - - @IBAction func textHorizontalShiftChanged(_ sender: UITextField) { - if let value = sender.text.flatMap({ Double($0) }) { - WatchLayout.shared.horizontalTextOffset = value - WatchFaceView.currentInstance?.drawView(forceRefresh: true) - } else { - sender.text = nil - } - } -} - -final class HelpViewController: UIViewController { - @IBOutlet var stackView: UIStackView! - private let parser = MarkdownParser() - - func boldText(line: String, fontSize: CGFloat) -> NSAttributedString { - let boldRanges = line.boldRanges - let attrStr = NSMutableAttributedString() - if !boldRanges.isEmpty { - var boldRangesIndex = boldRanges.startIndex - var startIndex = line.startIndex - while boldRangesIndex < boldRanges.endIndex { - let boldRange = boldRanges[boldRangesIndex] - let plainText = line[startIndex.. UIView in - let footnote = UILabel(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 21)) - footnote.text = NSLocalizedString("短按換主題,長按易名", comment: "Comment: tap to change theme, long press to rename") - footnote.textAlignment = .center - footnote.textColor = .secondaryLabel - footnote.font = .systemFont(ofSize: UIFont.smallSystemFontSize) - return footnote - }() - - loadThemes() - } - - @objc func refresh() { - loadThemes() - tableView.reloadData() - refreshControl!.endRefreshing() - } - - @objc func longPress(_ sender: UILongPressGestureRecognizer) { - if sender.state == .began { - let touchPoint = sender.location(in: tableView) - if let indexPath = tableView.indexPathForRow(at: touchPoint) { - if indexPath.section > 0 { - let cell = (tableView.cellForRow(at: indexPath) as! ThemeCell) - let alertController = UIAlertController(title: NSLocalizedString("易名", comment: "rename"), message: NSLocalizedString("不得爲空,不得重名", comment: "no blank, no duplicate name"), preferredStyle: .alert) - let renameAction = UIAlertAction(title: NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), style: .default) { _ in - DataContainer.shared.renameSave(name: cell.title!, deviceName: cell.deviceName!, newName: alertController.textFields![0].text!) - self.refresh() - } - let cancelAction = UIAlertAction(title: NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), style: .default) - alertController.addTextField { textField in - textField.text = cell.title - textField.addTarget(self, action: #selector(self.validateName(_:)), for: .editingChanged) - } - alertController.addAction(cancelAction) - alertController.addAction(renameAction) - alertController.actions[1].isEnabled = false - present(alertController, animated: true, completion: nil) - } - } - } - } - - func loadThemes() { - themes = [:] - let loadedThemes = DataContainer.shared.listAll() - for theme in loadedThemes { - if themes[theme.deviceName] == nil { - themes[theme.deviceName] = [theme] - } else { - themes[theme.deviceName]!.append(theme) - } - } - for deviceName in themes.keys { - themes[deviceName]!.sort { $0.modifiedDate > $1.modifiedDate } - } - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return themes.count + 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return 1 - } else { - let keys = themes.keys.sorted { $0 == currentDeviceName || $0 > $1 } - let key = keys[keys.index(keys.startIndex, offsetBy: section - 1)] - return themes[key]?.count ?? 0 - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if section == 0 { - return nil - } else { - let keys = themes.keys.sorted { $0 == currentDeviceName || $0 > $1 } - return keys[keys.index(keys.startIndex, offsetBy: section - 1)] - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0 { - let cell = ThemeCell() - cell.title = NSLocalizedString("謄錄", comment: "Save layout") - return cell - } else { - let keys = themes.keys.sorted { $0 == currentDeviceName || $0 > $1 } - let key = keys[keys.index(keys.startIndex, offsetBy: indexPath.section - 1)] - let cell = tableView.dequeueReusableCell(withIdentifier: ThemeCell.identifier, for: indexPath) as! ThemeCell - if let theme = themes[key]?[indexPath.row] { - cell.title = theme.name - cell.date = theme.modifiedDate - cell.deviceName = theme.deviceName - } - return cell - } - } - - override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - if indexPath.section == 0 { - return .insert - } else { - return .delete - } - } - - // Select - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - if indexPath.section > 0 { - let cell = (tableView.cellForRow(at: indexPath) as! ThemeCell) - - let alertController = UIAlertController(title: NSLocalizedString("換主題", comment: "Confirm to select theme title"), message: NSLocalizedString("換爲:", comment: "Confirm to select theme message") + (cell.title ?? ""), preferredStyle: .alert) - let cancelAction = UIAlertAction(title: NSLocalizedString("容吾三思", comment: "Cancel Resetting Settings"), style: .default) - let confirmAction = UIAlertAction(title: NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings"), style: .destructive) { [self] _ in - DataContainer.shared.loadSave(name: cell.title, deviceName: cell.deviceName) - (WatchFaceView.currentInstance?.window?.rootViewController as? ViewController)?.resize() - (navigationController?.viewControllers.first as? SettingsViewController)?.reload() - } - - alertController.addAction(cancelAction) - alertController.addAction(confirmAction) - present(alertController, animated: true, completion: nil) - - DataContainer.shared.loadSave(name: cell.title, deviceName: cell.deviceName) - - // New - } else { - let alertController = UIAlertController(title: NSLocalizedString("取名", comment: "set a name"), message: NSLocalizedString("不得爲空,不得重名", comment: "no blank, no duplicate name"), preferredStyle: .alert) - let addNewAction = UIAlertAction(title: NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), style: .default) { _ in - DataContainer.shared.saveLayout(WatchLayout.shared.encode(), name: alertController.textFields![0].text) - self.refresh() - } - let cancelAction = UIAlertAction(title: NSLocalizedString("容吾三思", comment: "Cancel adding Settings"), style: .default) - alertController.addTextField { [self] textField in - textField.text = generateNewName(baseName: NSLocalizedString("無名", comment: "new theme default name")) - textField.addTarget(self, action: #selector(self.validateName(_:)), for: .editingChanged) - } - alertController.addAction(cancelAction) - alertController.addAction(addNewAction) - alertController.actions[1].isEnabled = false - present(alertController, animated: true, completion: nil) - } - } - - func generateNewName(baseName: String) -> String { - var newFileName = baseName - guard let currentDeviceThemes = (themes[currentDeviceName]?.map { $0.name }) else { return baseName } - var i = 2 - while currentDeviceThemes.contains(newFileName) { - newFileName = baseName + " \(i)" - i += 1 - } - return newFileName - } - - @objc func validateName(_ sender: UITextField) { - var resp: UIResponder! = sender - while !(resp is UIAlertController) { resp = resp.next } - let alert = resp as! UIAlertController - if let fileName = sender.text, fileName != "" { - let currentDeviceThemes = themes[currentDeviceName] - if currentDeviceThemes == nil || !(currentDeviceThemes!.map { $0.name }.contains(fileName)) { - alert.actions[1].isEnabled = true - return - } - } - alert.actions[1].isEnabled = false - } - - // Delete - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let cell = (tableView.cellForRow(at: indexPath) as! ThemeCell) - - let alertController = UIAlertController(title: NSLocalizedString("刪主題", comment: "Confirm to delete theme title"), message: NSLocalizedString("刪:", comment: "Confirm to delete theme message") + (cell.title ?? ""), preferredStyle: .alert) - let cancelAction = UIAlertAction(title: NSLocalizedString("容吾三思", comment: "Cancel Resetting Settings"), style: .default) - let confirmAction = UIAlertAction(title: NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings"), style: .destructive) { _ in - DataContainer.shared.deleteSave(name: cell.title!, deviceName: cell.deviceName!) - self.loadThemes() - self.tableView.reloadData() - } - - alertController.addAction(cancelAction) - alertController.addAction(confirmAction) - present(alertController, animated: true, completion: nil) - } - } -} diff --git a/iOS/Views/Setting.swift b/iOS/Views/Setting.swift new file mode 100644 index 0000000..31e3c3e --- /dev/null +++ b/iOS/Views/Setting.swift @@ -0,0 +1,125 @@ +// +// Setting.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/25/23. +// + +import SwiftUI + +struct Setting: View { + @State var locationManager = LocationManager.shared + @Environment(\.chineseCalendar) var chineseCalendar + @Environment(\.watchSetting) var watchSetting + @Environment(\.watchLayout) var watchLayout + + var body: some View { + List { + Section(header: Text("數據", comment: "Data Source")) { + NavigationLink { + Datetime() + } label: { + HStack { + Label { + Text("日時", comment: "Display time settings") + } icon: { + Image(systemName: "clock") + } + Spacer() + let timezone = watchSetting.timezone ?? Calendar.current.timeZone + Text("\((watchSetting.displayTime ?? chineseCalendar.time).formatted(date: .numeric, time: .shortened)) \(timezone.localizedName(for: .generic, locale: Locale.current) ?? "")") + .minimumScaleFactor(0.75) + .foregroundStyle(.secondary) + .frame(alignment: .leading) + } + } + + NavigationLink { + Location() + + } label: { + HStack { + Label { + Text("經緯度", comment: "Geo Location section") + } icon: { + Image(systemName: "location") + } + Spacer() + if let location = locationManager.location ?? watchLayout.location { + let (lat, lon) = coordinateDesp(coordinate: location) + Text("\(lat), \(lon)") + .privacySensitive() + .minimumScaleFactor(0.75) + .foregroundStyle(.secondary) + .frame(alignment: .leading) + } + } + } + } + + Section(header: Text("樣式", comment: "Styles")) { + NavigationLink{ + RingSetting() + } label: { + Label { + Text("輪色", comment: "Rings Color Setting") + } icon: { + Image(systemName: "pencil.and.outline") + } + } + NavigationLink { + ColorSetting() + } label: { + Label { + Text("色塊", comment: "Mark Color settings") + } icon: { + Image(systemName: "wand.and.stars") + } + } + NavigationLink { + LayoutSetting() + } label: { + Label { + Text("佈局", comment: "Layout settings section") + } icon: { + Image(systemName: "square.resize") + } + } + } + Section(header: Text("其它", comment: "Miscellaneous")) { + NavigationLink { + ThemesList() + } label: { + Label { + Text("主題庫", comment: "manage saved themes") + } icon: { + Image(systemName: "archivebox") + } + } + NavigationLink { + Documentation() + } label: { + Label { + Text("註釋", comment: "Documentation View") + } icon: { + Image(systemName: "doc.questionmark") + } + } + } + } + .navigationTitle(Text("設置", comment: "Settings View")) + .navigationBarTitleDisplayMode(.large) + .toolbar { + Button(NSLocalizedString("畢", comment: "Close settings panel")) { + watchSetting.presentSetting = false + } + .fontWeight(.semibold) + } + } +} + +#Preview("Settings") { + NavigationStack { + Setting() + } +} diff --git a/iOS/Views/WatchFace.swift b/iOS/Views/WatchFace.swift new file mode 100644 index 0000000..d700877 --- /dev/null +++ b/iOS/Views/WatchFace.swift @@ -0,0 +1,135 @@ +// +// WatchFaceView.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/25/23. +// + +import SwiftUI +import WidgetKit + +@MainActor +struct WatchFace: View { + @Environment(\.chineseCalendar) var chineseCalendar + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + @Environment(\.scenePhase) var scenePhase + @Environment(\.modelContext) private var modelContext + @State var showWelcome = false + @State var entityPresenting = EntitySelection() + @State var tapPos: CGPoint? = nil + @State var hoverBounds: CGRect = .zero + @GestureState var longPressed = false + var presentSetting: Binding { + .init(get: { watchSetting.presentSetting }, set: { newValue in + watchSetting.presentSetting = newValue + if !newValue { + 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) + } + } + } + } + + 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 mainSize(proxy: GeometryProxy) -> CGSize { + var idealSize = watchLayout.watchSize + if proxy.size.height < proxy.size.width { + let width = idealSize.width + idealSize.width = idealSize.height + idealSize.height = width + } + if proxy.size.width * 0.95 < idealSize.width { + let ratio = proxy.size.width * 0.95 / idealSize.width + idealSize.width *= ratio + idealSize.height *= ratio + } + if proxy.size.height * 0.95 < idealSize.height { + let ratio = proxy.size.height * 0.95 / idealSize.height + idealSize.width *= ratio + idealSize.height *= ratio + } + return idealSize + } + + var body: some View { + GeometryReader { proxy in + let size = mainSize(proxy: proxy) + let centerOffset = if size.height >= size.width { + watchLayout.centerTextOffset + } else { + watchLayout.centerTextHOffset + } + + 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) + .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)) + + Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) + } + .onChange(of: proxy.size) { _, newSize in + watchSetting.vertical = newSize.height >= newSize.width + } + .animation(.easeInOut(duration: 0.2), value: entityPresenting.activeNote) + } + .sheet(isPresented: $showWelcome) { + Welcome() + } + .inspector(isPresented: presentSetting) { + Setting() + .presentationBackground(.thinMaterial) + .inspectorColumnWidth(min: 350, ideal: 400, max: 500) + } + .task(priority: .background) { + showWelcome = ThemeData.latestVersion() < ThemeData.version + } + .onChange(of: scenePhase) { _, newPhase in + switch newPhase { + case .inactive, .background: + WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) + WidgetCenter.shared.reloadAllTimelines() + watchLayout.saveDefault(context: modelContext) + try? modelContext.save() + default: + break + } + } + } +} + +#Preview("Watch Face") { + WatchFace() +} diff --git a/iOS/Views/Welcome.swift b/iOS/Views/Welcome.swift new file mode 100644 index 0000000..48ca148 --- /dev/null +++ b/iOS/Views/Welcome.swift @@ -0,0 +1,80 @@ +// +// Welcome.swift +// Chinese Time iOS +// +// Created by Leo Liu on 6/23/23. +// + +import SwiftUI + +struct Welcome: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack { + ScrollView { + VStack(spacing: 20) { + Spacer(minLength: 10) + .frame(maxHeight: 20) + Image(.image) + .resizable() + .frame(width: 120, height: 120) + Text("華曆", comment: "Chinese Time") + .font(.largeTitle.bold()) + Spacer(minLength: 10) + .frame(maxHeight: 20) + VStack(spacing: 20) { + HStack { + Image(systemName: "pencil.circle.fill") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("輪式設計", comment: "Welcome, ring design - title") + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 20) + .frame(maxWidth: .infinity, alignment: .leading) + Text("採用錶盤式設計,不同於以往日曆形制。一年、一月、一日、一時均週而復始,最適以「輪」代表。呈現細節之外,亦不失大局。", comment: "Welcome, ring design - detail") + .font(.subheadline) + } + } + HStack { + Image(systemName: "gearshape.fill") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("長按錶盤進設置", comment: "Welcome, long press - title") + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 20) + .frame(maxWidth: .infinity, alignment: .leading) + Text("設置更改後自動保存。可調時間、在地、外觀等。另有更多有關華曆之介紹。", comment: "Welcome, long press - detail") + .font(.subheadline) + } + } + } + } + } + Spacer(minLength: 15) + .frame(maxHeight: 25) + + Button { + dismiss() + } label: { + Text("閱", comment: "Ok") + .frame(maxWidth: .infinity) + } + .controlSize(.large) + .buttonStyle(.borderedProminent) + .buttonBorderShape(.roundedRectangle(radius: 15)) + } + .padding() + } +} + + +#Preview("Welcome") { + Welcome() +} diff --git a/iOS/WatchFace.swift b/iOS/WatchFace.swift deleted file mode 100644 index 6763ccd..0000000 --- a/iOS/WatchFace.swift +++ /dev/null @@ -1,230 +0,0 @@ -// -// watchFace.swift -// ChineseTime -// -// Created by LEO Yoon-Tsaw on 9/19/21. -// - -import UIKit - -final class WatchFaceView: UIView { - private static let majorUpdateInterval: CGFloat = 3600 - private static let minorUpdateInterval: CGFloat = majorUpdateInterval / 12 - private static let updateInterval: CGFloat = 14.4 - static let frameOffset: CGFloat = 5 - static var currentInstance: WatchFaceView? - - let watchLayout = WatchLayout.shared - var displayTime: Date? = nil - var timezone: TimeZone = Calendar.current.timeZone - var phase: StartingPhase = .init(zeroRing: 0, firstRing: 0, secondRing: 0, thirdRing: 0, fourthRing: 0) - var timer: Timer? - var entityNotes: [EntityNote] = [] - - var location: CGPoint? { - LocationManager.shared.location ?? watchLayout.location - } - - private var chineseCalendar = ChineseCalendar(time: Date(), timezone: TimeZone.current, location: nil) - - var graphicArtifects = GraphicArtifects() - private var keyStates = KeyStates() - - override init(frame frameRect: CGRect) { - super.init(frame: frameRect) - Self.currentInstance = self - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - Self.currentInstance = self - } - - func setAutoRefresh() { - timer = Timer.scheduledTimer(withTimeInterval: Self.updateInterval, repeats: true) { _ in - self.drawView(forceRefresh: false) - } - } - - var isDark: Bool { - traitCollection.userInterfaceStyle == .dark - } - - func update() { - let time = displayTime ?? Date() - chineseCalendar.update(time: time, timezone: timezone, location: location) - } - - func drawView(forceRefresh: Bool) { - layer.sublayers = [] - if forceRefresh { - let _ = WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) - graphicArtifects = GraphicArtifects() - } - update() - setNeedsDisplay() - } - - func updateSize(with frame: CGRect) { - self.frame = frame - drawView(forceRefresh: true) - } - - override func draw(_ rawRect: CGRect) { - let dirtyRect = rawRect.insetBy(dx: Self.frameOffset, dy: Self.frameOffset) - entityNotes = layer.update(dirtyRect: dirtyRect, isDark: isDark, watchLayout: watchLayout, chineseCalendar: chineseCalendar, graphicArtifects: graphicArtifects, keyStates: keyStates, phase: phase) - } -} - -final class NoteView: UIView { - private var visualEffectView: UIVisualEffectView! - private var entities: [EntityNote] = [] - - init?(center: CGPoint, bounds: CGRect, entities: [EntityNote]) { - var entities = entities - let width: CGFloat - let height: CGFloat - if Locale.isChinese { - width = CGFloat(entities.count) * (UIFont.systemFontSize + 8) + 8 - height = CGFloat(entities.map { $0.name.count }.reduce(0) { max($0, $1) }) * (UIFont.systemFontSize + 6) + 30 - } else { - for i in 0 ..< entities.count { - entities[i].name = Locale.translation[entities[i].name] ?? entities[i].name - } - width = CGFloat(entities.map { NSAttributedString(string: $0.name, attributes: [.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]).boundingRect(with: .zero, context: .none).width }.reduce(0) { max($0, $1) }) * 1.2 + 30 - height = CGFloat(entities.count) * (UIFont.systemFontSize + 9) + 3 - } - var frame = CGRect(x: center.x - width / 2, y: center.y - height / 2, width: width, height: height) - - if frame.maxX > bounds.maxX { - frame.origin.x -= frame.maxX - bounds.maxX - } - if frame.maxY > bounds.maxY { - frame.origin.y -= frame.maxY - bounds.maxY - } - if frame.minX >= bounds.minX && frame.minY >= bounds.minY { - self.entities = entities - super.init(frame: frame) - layer.shadowOffset = CGSize(width: 3, height: -3) - layer.shadowRadius = 5 - layer.shadowOpacity = 0.2 - layer.shadowColor = UIColor.black.cgColor - setupView() - } else { - return nil - } - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - alpha = 0 - UIView.animate(withDuration: 0.2) { - self.alpha = 1.0 - } - Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in - UIView.animate(withDuration: 0.2, animations: { - self.alpha = 0.0 - }) { _ in - self.removeFromSuperview() - } - } - } - - private func setupView() { - let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial) - visualEffectView = UIVisualEffectView(effect: blurEffect) - addSubview(visualEffectView) - - visualEffectView.layer.masksToBounds = true - visualEffectView.frame = bounds - let mask = CAShapeLayer() - mask.path = RoundedRect(rect: bounds, nodePos: 10, ankorPos: 2).path - visualEffectView.layer.mask = mask - - let isChinese = Locale.isChinese - var lastView: UIView? = nil - for entity in entities.reversed() { - let entityView = createEntityView(for: entity, isChinese: isChinese) - visualEffectView.contentView.addSubview(entityView) - - entityView.translatesAutoresizingMaskIntoConstraints = false - if isChinese { - if let lastView = lastView { - entityView.trailingAnchor.constraint(equalTo: lastView.leadingAnchor, constant: -6).isActive = true - } else { - entityView.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -6).isActive = true - } - entityView.topAnchor.constraint(equalTo: visualEffectView.topAnchor, constant: 6).isActive = true - entityView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -6).isActive = true - entityView.widthAnchor.constraint(equalToConstant: UIFont.systemFontSize + 2).isActive = true - } else { - if let lastView = lastView { - entityView.bottomAnchor.constraint(equalTo: lastView.topAnchor, constant: -2).isActive = true - } else { - entityView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -4).isActive = true - } - entityView.leadingAnchor.constraint(equalTo: visualEffectView.leadingAnchor, constant: 6).isActive = true - entityView.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -6).isActive = true - entityView.heightAnchor.constraint(equalToConstant: UIFont.systemFontSize + 6).isActive = true - } - - lastView = entityView - } - } - - private func createEntityView(for entity: EntityNote, isChinese: Bool) -> UIView { - let view = UIView() - - let colorMark = UIView() - colorMark.layer.backgroundColor = entity.color - let mask = CAShapeLayer() - mask.path = RoundedRect(rect: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: 11, height: 11)), nodePos: 0.7 * 6, ankorPos: 0.3 * 6).path - colorMark.layer.mask = mask - view.addSubview(colorMark) - - let label = UILabel() - view.addSubview(label) - colorMark.translatesAutoresizingMaskIntoConstraints = false - label.translatesAutoresizingMaskIntoConstraints = false - - if isChinese { - label.text = entity.name.map { String($0) }.joined(separator: "\n") - label.textAlignment = .right - label.numberOfLines = 0 - - NSLayoutConstraint.activate([ - colorMark.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -3), - colorMark.topAnchor.constraint(equalTo: view.topAnchor, constant: 2), - colorMark.widthAnchor.constraint(equalToConstant: 12), - colorMark.heightAnchor.constraint(equalToConstant: 12), - - label.topAnchor.constraint(equalTo: colorMark.bottomAnchor, constant: 4), - label.trailingAnchor.constraint(equalTo: view.trailingAnchor), - label.widthAnchor.constraint(equalTo: view.widthAnchor) - ]) - - } else { - label.text = entity.name - label.textAlignment = .left - label.numberOfLines = 1 - - NSLayoutConstraint.activate([ - colorMark.topAnchor.constraint(equalTo: view.topAnchor, constant: 4.5), - colorMark.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2), - colorMark.widthAnchor.constraint(equalToConstant: 12), - colorMark.heightAnchor.constraint(equalToConstant: 12), - - label.leadingAnchor.constraint(equalTo: colorMark.trailingAnchor, constant: 4), - label.bottomAnchor.constraint(equalTo: view.bottomAnchor), - label.heightAnchor.constraint(equalTo: view.heightAnchor) - ]) - } - - return view - } -} diff --git a/iOS/en.lproj/InfoPlist.strings b/iOS/en.lproj/InfoPlist.strings deleted file mode 100644 index 3a2ec98..0000000 --- a/iOS/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "Chinese Time"; - -/* Bundle name */ -"CFBundleName" = "Chinese Time"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/iOS/en.lproj/Localizable.strings b/iOS/en.lproj/Localizable.strings deleted file mode 100644 index acd77d7..0000000 --- a/iOS/en.lproj/Localizable.strings +++ /dev/null @@ -1,130 +0,0 @@ -/* Default save file name */ -"Default" = "Default"; - -/* no blank, no duplicate name */ -"不得爲空,不得重名" = "No empty name, no duplicated name"; - -/* manage saved layouts - manage saved themes */ -"主題庫" = "Saved Themes"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# Why create this Chinese Time?\nIs 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, 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 亥."; - -/* Layout Parameter View - Layout parameters */ -"佈局" = "Layouts"; - -/* Ok */ -"作罷" = "OK"; - -/* Action */ -"其它" = "OTHERS"; - -/* Confirm to delete theme message */ -"刪:" = "Delete: "; - -/* Confirm to delete theme title */ -"刪主題" = "Delete Theme"; - -/* Pull to refresh */ -"刷新" = "Refresh"; - -/* set a name */ -"取名" = "Set Name"; - -/* Confirm Resetting Settings */ -"吾意已決" = "Proceed"; - -/* Circle Color View - Circle colors */ -"圈色" = "Ring Colors"; - -/* Mark Color View - Mark colors */ -"塊標色" = "Mark Colors"; - -/* Cancel adding Settings - Cancel Resetting Settings */ -"容吾三思" = "Cancel"; - -/* Location not enabled but tried to locate title */ -"怪哉" = "Sorry"; - -/* Confirm to select theme title */ -"換主題" = "Switch Theme"; - -/* Confirm to select theme message */ -"換爲:" = "Switch to: "; - -/* Data Source */ -"數據" = "Data"; - -/* rename */ -"易名" = "Rename"; - -/* Time setting */ -"時間" = "Solar Time"; - -/* Time setting: mean solar time */ -"標準時" = "Standard Time"; - -/* Styles */ -"樣式" = "Styles"; - -/* Confirm adding Settings */ -"此名甚善" = "Confirm"; - -/* Help Doc */ -"注釋" = "Documentation"; - -/* new theme default name */ -"無名" = "Unnamed"; - -/* Close settings panel */ -"畢" = "Done"; - -/* Time setting: apparent solar time */ -"真太陽時" = "Apparent S Time"; - -/* Comment: tap to change theme, long press to rename */ -"短按換主題,長按易名" = "Tap to switch theme, long press to rename"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - -/* Leap month setting: daily precision */ -"精確至日" = "Daily Precision"; - -/* Leap month setting: precise */ -"精確至時刻" = "Finest Precision"; - -/* Location - Location View */ -"經緯度" = "Geo Location"; - -/* Leap month setting */ -"置閏法" = "Leap Month"; - -/* Location not enabled but tried to locate message */ -"蓋因定位未開啓" = "Location service is disabled"; - -/* Location fails to load */ -"虚無" = "Location not Found"; - -/* Settings View */ -"設置" = "Settings"; - -/* Details about Settings */ -"設置介紹" = "Changes made to settings will auto save. Customize for display time, location and layouts. Find more about Chinese Time in Settings"; - -/* Save layout */ -"謄錄" = "Save Current"; - -/* Details about Ring Design */ -"輪試設計介紹" = "Watch face like design, different from normal calendar, can best convey the idea of celestial circular movements. And you will not lose the full picture when focusing on precision."; - -/* Display time - Display Time View */ -"顯示時間" = "Display Time"; - diff --git a/iOS/en.lproj/Main.strings b/iOS/en.lproj/Main.strings deleted file mode 100644 index ec030a6..0000000 --- a/iOS/en.lproj/Main.strings +++ /dev/null @@ -1,237 +0,0 @@ -/* Class = "UILabel"; text = "日入色"; ObjectID = "0e0-lo-OGc"; */ -"0e0-lo-OGc.text" = "Sunset"; - -/* Class = "UILabel"; text = "朔望節氣"; ObjectID = "1HR-Ey-12e"; */ -"1HR-Ey-12e.text" = "MOON & SOLAR TERMS"; - -/* Class = "UILabel"; text = "小字平移"; ObjectID = "1hu-xy-9gx"; */ -"1hu-xy-9gx.text" = "Text H Offset"; - -/* Class = "UILabel"; text = "內核色"; ObjectID = "1u3-HL-g6u"; */ -"1u3-HL-g6u.text" = "Core Back"; - -/* Class = "UILabel"; text = "圓角比例"; ObjectID = "2U0-7s-zRr"; */ -"2U0-7s-zRr.text" = "Corner Radius"; - -/* Class = "UILabel"; text = "日時"; ObjectID = "3FT-tc-vrB"; */ -"3FT-tc-vrB.text" = "Date & Time"; - -/* Class = "UILabel"; text = "小刻透明"; ObjectID = "5bx-eq-Oxv"; */ -"5bx-eq-Oxv.text" = "Minor Tick Transparency"; - -/* Class = "UILabel"; text = "辰星色"; ObjectID = "5H0-kn-rEg"; */ -"5H0-kn-rEg.text" = "Mercury"; - -/* Class = "UILabel"; text = "1.0"; ObjectID = "5w6-wZ-710"; */ -"5w6-wZ-710.text" = "1.0"; - -/* Class = "UILabel"; text = "大字平移"; ObjectID = "6HB-Mc-8Vy"; */ -"6HB-Mc-8Vy.text" = "Center Text H Offset"; - -/* Class = "UILabel"; text = "年圈色"; ObjectID = "6Rp-wy-EDP"; */ -"6Rp-wy-EDP.text" = "Year Ring"; - -/* Class = "UILabel"; text = "太白色"; ObjectID = "7Ag-rX-8vz"; */ -"7Ag-rX-8vz.text" = "Venus"; - -/* Class = "UILabel"; text = "明"; ObjectID = "7GV-3Q-HxP"; */ -"7GV-3Q-HxP.text" = "Light"; - -/* Class = "UILabel"; text = "氣標記色"; ObjectID = "9Bs-fx-Mqx"; */ -"9Bs-fx-Mqx.text" = "EST Mark"; - -/* Class = "UILabel"; text = "殘圈透明"; ObjectID = "9O4-pc-7B4"; */ -"9O4-pc-7B4.text" = "Inactive Ring Transparency"; - -/* Class = "UILabel"; text = "輪式設計"; ObjectID = "46h-V0-3UW"; */ -"46h-V0-3UW.text" = "Ring Design"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "72l-Zb-OQ3"; */ -"72l-Zb-OQ3.text" = "Dark"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "89r-Ln-eZg"; */ -"89r-Ln-eZg.text" = "Dark"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "93w-GC-vSC"; */ -"93w-GC-vSC.text" = "Loop"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "ayK-26-pLM"; */ -"ayK-26-pLM.text" = "Dark"; - -/* Class = "UILabel"; text = "大刻透明"; ObjectID = "Btj-qO-PGB"; */ -"Btj-qO-PGB.text" = "Major Tick Transparency"; - -/* Class = "UILabel"; text = "鎮星色"; ObjectID = "bXc-NQ-BtY"; */ -"bXc-NQ-BtY.text" = "Saturn"; - -/* Class = "UILabel"; text = "望標記色"; ObjectID = "cN5-cm-WtA"; */ -"cN5-cm-WtA.text" = "Full Moon"; - -/* Class = "UILabel"; text = "華曆"; ObjectID = "CpE-Ch-tZF"; */ -"CpE-Ch-tZF.text" = "Chinese Time"; - -/* Class = "UILabel"; text = "大字色"; ObjectID = "ctU-7S-kE1"; */ -"ctU-7S-kE1.text" = "Center Text Color"; - -/* Class = "UILabel"; text = "月圈色"; ObjectID = "CZS-0s-tDB"; */ -"CZS-0s-tDB.text" = "Month Ring"; - -/* Class = "UINavigationItem"; title = "設置"; ObjectID = "dgF-Pn-a5p"; */ -"dgF-Pn-a5p.title" = "Settings"; - -/* Class = "UILabel"; text = "月出入"; ObjectID = "dYQ-sw-ZS7"; */ -"dYQ-sw-ZS7.text" = "MOONRISE/SET"; - -/* Class = "UILabel"; text = "今時"; ObjectID = "eoa-tt-OR9"; */ -"eoa-tt-OR9.text" = "Now"; - -/* Class = "UILabel"; text = "日圈色"; ObjectID = "evD-bV-0Hj"; */ -"evD-bV-0Hj.text" = "Day Ring"; - -/* Class = "UITextView"; text = "採用錶盤式設計,不同於以往日曆形制。一年、一月、一日、一時均週而復始,最適以「輪」代表。呈現細節之外,亦不失大局。"; ObjectID = "fRL-Ka-BEX"; */ -"fRL-Ka-BEX.text" = "Watch face like design, different from normal calendar, can best convey the idea of celestial circular movements. And you will not lose the full picture when focusing on precision."; - -/* Class = "UILabel"; text = "寬"; ObjectID = "gLV-vh-zjy"; */ -"gLV-vh-zjy.text" = "Width"; - -/* Class = "UILabel"; text = "月中色"; ObjectID = "gQD-xr-xf8"; */ -"gQD-xr-xf8.text" = "Lunar Noon"; - -/* Class = "UILabel"; text = "大刻色"; ObjectID = "h2X-O3-Q0s"; */ -"h2X-O3-Q0s.text" = "Major Tick"; - -/* Class = "UILabel"; text = "朔標記色"; ObjectID = "h92-Li-DOy"; */ -"h92-Li-DOy.text" = "New Moon"; - -/* Class = "UILabel"; text = "月出色"; ObjectID = "HeB-cG-0Dr"; */ -"HeB-cG-0Dr.text" = "Moonrise"; - -/* Class = "UILabel"; text = "月入色"; ObjectID = "hfx-at-bvt"; */ -"hfx-at-bvt.text" = "Moonset"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "Hk3-HK-Q5L"; */ -"Hk3-HK-Q5L.text" = "Loop"; - -/* Class = "UILabel"; text = "五星"; ObjectID = "hs2-Ba-gBd"; */ -"hs2-Ba-gBd.text" = "PLANETS"; - -/* Class = "UILabel"; text = "明"; ObjectID = "iCh-Iu-Fvd"; */ -"iCh-Iu-Fvd.text" = "Light"; - -/* Class = "UILabel"; text = "長按錶盤進設置"; ObjectID = "Ika-1B-exq"; */ -"Ika-1B-exq.text" = "Long press to enter Settings"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "iL5-OW-xD2"; */ -"iL5-OW-xD2.text" = "Dark"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "IXz-vg-R7g"; */ -"IXz-vg-R7g.text" = "Dark"; - -/* Class = "UILabel"; text = "熒惑色"; ObjectID = "iZi-62-FRb"; */ -"iZi-62-FRb.text" = "Mars"; - -/* Class = "UILabel"; text = "中氣色"; ObjectID = "JoB-wn-GrI"; */ -"JoB-wn-GrI.text" = "Even S Term"; - -/* Class = "UILabel"; text = "小字色"; ObjectID = "JtQ-6v-rbs"; */ -"JtQ-6v-rbs.text" = "Text Color"; - -/* Class = "UILabel"; text = "小字縱移"; ObjectID = "KGD-r8-uTM"; */ -"KGD-r8-uTM.text" = "Text V Offset"; - -/* Class = "UILabel"; text = "明"; ObjectID = "KPQ-Y5-gYy"; */ -"KPQ-Y5-gYy.text" = "Light"; - -/* Class = "UILabel"; text = "透明度"; ObjectID = "L9j-Kz-U52"; */ -"L9j-Kz-U52.text" = "TRANSPARENCY"; - -/* Class = "UILabel"; text = "明"; ObjectID = "LeU-hh-otX"; */ -"LeU-hh-otX.text" = "Light"; - -/* Class = "UISegmentedControl"; LtQ-H1-z6G.segmentTitles[0] = "手錄"; ObjectID = "LtQ-H1-z6G"; */ -"LtQ-H1-z6G.segmentTitles[0]" = "Manual"; - -/* Class = "UISegmentedControl"; LtQ-H1-z6G.segmentTitles[1] = "今地"; ObjectID = "LtQ-H1-z6G"; */ -"LtQ-H1-z6G.segmentTitles[1]" = "GPS"; - -/* Class = "UILabel"; text = "大字縱移"; ObjectID = "lVa-YZ-exC"; */ -"lVa-YZ-exC.text" = "Center Text V Offset"; - -/* Class = "UILabel"; text = "月色"; ObjectID = "Lwn-br-yo9"; */ -"Lwn-br-yo9.text" = "Moon"; - -/* Class = "UILabel"; text = "在地"; ObjectID = "m39-NT-rXb"; */ -"m39-NT-rXb.text" = "Enabled"; - -/* Class = "UILabel"; text = "0.25"; ObjectID = "Mto-Hp-LF8"; */ -"Mto-Hp-LF8.text" = "0.25"; - -/* Class = "UILabel"; text = "日出色"; ObjectID = "mtt-kV-gTE"; */ -"mtt-kV-gTE.text" = "Sunrise"; - -/* Class = "UILabel"; text = "高"; ObjectID = "NMv-Vw-KIV"; */ -"NMv-Vw-KIV.text" = "Height"; - -/* Class = "UILabel"; text = "經度"; ObjectID = "NR0-7n-bua"; */ -"NR0-7n-bua.text" = "Longitude"; - -/* Class = "UILabel"; text = "時區"; ObjectID = "oAy-Fp-oEE"; */ -"oAy-Fp-oEE.text" = "Time Zone"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "oxC-5l-WpM"; */ -"oxC-5l-WpM.text" = "Loop"; - -/* Class = "UILabel"; text = "明"; ObjectID = "PHO-EE-lP5"; */ -"PHO-EE-lP5.text" = "Light"; - -/* Class = "UILabel"; text = "經緯度"; ObjectID = "pl1-Ib-blg"; */ -"pl1-Ib-blg.text" = "GEO LOCATION"; - -/* Class = "UILabel"; text = "明"; ObjectID = "pUm-7e-ynh"; */ -"pUm-7e-ynh.text" = "Light"; - -/* Class = "UILabel"; text = "日出入"; ObjectID = "qiK-yl-54u"; */ -"qiK-yl-54u.text" = "SUNRISE/SET"; - -/* Class = "UILabel"; text = "漸變色"; ObjectID = "RuP-Yw-XoK"; */ -"RuP-Yw-XoK.text" = "GRADIENTS"; - -/* Class = "UILabel"; text = "夜中色"; ObjectID = "slb-Jl-hOj"; */ -"slb-Jl-hOj.text" = "Midnight"; - -/* Class = "UILabel"; text = "明暗主題色"; ObjectID = "T90-0P-AQ1"; */ -"T90-0P-AQ1.text" = "TICK & TEXT COLORS"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "u6s-lC-FRG"; */ -"u6s-lC-FRG.text" = "Dark"; - -/* Class = "UIButton"; configuration.title = "閱"; ObjectID = "UgU-NM-Xad"; */ -"UgU-NM-Xad.configuration.title" = "OK"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "UgU-NM-Xad"; */ -"UgU-NM-Xad.normalTitle" = "Button"; - -/* Class = "UILabel"; text = "歲星色"; ObjectID = "v5B-62-oMP"; */ -"v5B-62-oMP.text" = "Jupyter"; - -/* Class = "UILabel"; text = "緯度"; ObjectID = "VJ5-0u-4yr"; */ -"VJ5-0u-4yr.text" = "Latitude"; - -/* Class = "UILabel"; text = "節標記色"; ObjectID = "Xbw-rN-8bt"; */ -"Xbw-rN-8bt.text" = "OST Mark"; - -/* Class = "UILabel"; text = "1.0"; ObjectID = "xhf-f2-zsv"; */ -"xhf-f2-zsv.text" = "1.0"; - -/* Class = "UITextView"; text = "設置更改後自動保存。可調時間、在地、外觀等。另有更多有關華曆之介紹。"; ObjectID = "zei-WM-ULJ"; */ -"zei-WM-ULJ.text" = "Changes made to settings will auto save. Customize for display time, location and layouts. Find more about Chinese Time in Settings"; - -/* Class = "UILabel"; text = "節色"; ObjectID = "ZHY-yC-gqU"; */ -"ZHY-yC-gqU.text" = "Odd S Term"; - -/* Class = "UILabel"; text = "日中色"; ObjectID = "zij-j0-s08"; */ -"zij-j0-s08.text" = "Solar Noon"; - -/* Class = "UILabel"; text = "小刻色"; ObjectID = "ZNL-AU-VER"; */ -"ZNL-AU-VER.text" = "Minor Tick"; - diff --git a/iOS/iOSApp.swift b/iOS/iOSApp.swift new file mode 100644 index 0000000..6553e1b --- /dev/null +++ b/iOS/iOSApp.swift @@ -0,0 +1,45 @@ +// +// AppDelegate.swift +// Chinese Time +// +// Created by Leo Liu on 4/17/23. +// + +import SwiftUI + +@main +struct ChineseTimeiOSApp: App { + let watchConnectivity = WatchConnectivityManager.shared + let chineseCalendar = ChineseCalendar(time: .now) + let locationManager = LocationManager.shared + let watchLayout = WatchLayout.shared + let watchSetting = WatchSetting.shared + let timer = Timer.publish(every: ChineseCalendar.updateInterval, on: .main, in: .common).autoconnect() + + init() { + let modelContext = ThemeData.container.mainContext + watchLayout.loadDefault(context: modelContext) + locationManager.requestLocation() + } + + var body: some Scene { + WindowGroup { + WatchFace() + .environment(\.chineseCalendar, chineseCalendar) + .modelContainer(ThemeData.container) + .task { + self.update() + await updateCountDownRelevantIntents(chineseCalendar: chineseCalendar.copy) + } + .onReceive(timer) { _ in + self.update() + } + } + } + + func update() { + chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, + timezone: watchSetting.timezone ?? Calendar.current.timeZone, + location: locationManager.location ?? watchLayout.location) + } +} diff --git a/iOS/layout.txt b/iOS/layout.txt index cdda9fb..ccb801f 100644 --- a/iOS/layout.txt +++ b/iOS/layout.txt @@ -1,5 +1,6 @@ globalMonth: false apparentTime: 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 secondRing: locations: 0.0, 0.25, 0.5, 0.75; colors: 0xFF8658C5, 0xFF6E68E7, 0xFF5283EF, 0xFF6E68E7; loop: true @@ -10,10 +11,10 @@ majorTickAlpha: 0.0 minorTickColor: 0x00000000 minorTickAlpha: 0.7 fontColor: 0xFFFFFFFF -centerFontColor: locations: 0.25, 0.75; colors: 0xFF665598, 0xFFA5698A; loop: false +centerFontColor: locations: 0.25, 0.75; colors: 0xFFA36497, 0xFFC28F8A; loop: false evenSolarTermTickColor: 0xFF000000 oddSolarTermTickColor: 0xFF555555 -innerColorDark: 0xFF000000 +innerColorDark: 0xFF101010 majorTickColorDark: 0x00000000 minorTickColorDark: 0x007F7F7F fontColorDark: 0xFF000000 @@ -27,9 +28,10 @@ evenStermIndicator: 0xFFFFFFFF sunPositionIndicator: 0xFF000000, 0xFFAAADFF, 0xFF0A36B1, 0xFF6EBCD3 moonPositionIndicator: 0xFFE6B8AF, 0xFFE56572, 0xFF9E1E67 shadeAlpha: 0.25 -centerTextOffset: 1.1 -centerTextHorizontalOffset: -0.1 -verticalTextOffset: -0.5 +shadowSize: 0.03 +centerTextOffset: 0.05 +centerTextHorizontalOffset: 0.05 +verticalTextOffset: 0.0 horizontalTextOffset: 0.0 watchWidth: 396.0 watchHeight: 484.0 diff --git a/iOS/zh-Hans.lproj/InfoPlist.strings b/iOS/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index 35814e5..0000000 --- a/iOS/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "华历"; - -/* Bundle name */ -"CFBundleName" = "华历"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/iOS/zh-Hans.lproj/Localizable.strings b/iOS/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index bb00041..0000000 --- a/iOS/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,130 +0,0 @@ -/* Default save file name */ -"Default" = "常备"; - -/* no blank, no duplicate name */ -"不得爲空,不得重名" = "不得为空,不得重名"; - -/* manage saved layouts - manage saved themes */ -"主題庫" = "主题库"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# 为何要做此华历?\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**五星**之古名与今不同。辰星即今之水星,又单名“星”;太白星即今之金星,以其为天空最亮之星,故名太白;荧惑即今之火星;岁星即今之木星,太岁乃与岁星相关但不同的概念;填星又作镇星,即今之土星。"; - -/* Layout Parameter View - Layout parameters */ -"佈局" = "布局"; - -/* Ok */ -"作罷" = "作罢"; - -/* Action */ -"其它" = "其它"; - -/* Confirm to delete theme message */ -"刪:" = "删:"; - -/* Confirm to delete theme title */ -"刪主題" = "删主题"; - -/* Pull to refresh */ -"刷新" = "刷新"; - -/* set a name */ -"取名" = "取名"; - -/* Confirm Resetting Settings */ -"吾意已決" = "吾意已决"; - -/* Circle Color View - Circle colors */ -"圈色" = "轮色"; - -/* Mark Color View - Mark colors */ -"塊標色" = "色块"; - -/* Cancel adding Settings - Cancel Resetting Settings */ -"容吾三思" = "容吾三思"; - -/* Location not enabled but tried to locate title */ -"怪哉" = "怪哉"; - -/* Confirm to select theme title */ -"換主題" = "换主题"; - -/* Confirm to select theme message */ -"換爲:" = "换为:"; - -/* Data Source */ -"數據" = "数据"; - -/* rename */ -"易名" = "易名"; - -/* Time setting */ -"時間" = "时间"; - -/* Time setting: mean solar time */ -"標準時" = "标准时"; - -/* Styles */ -"樣式" = "样式"; - -/* Confirm adding Settings */ -"此名甚善" = "此名甚善"; - -/* Help Doc */ -"注釋" = "注释"; - -/* new theme default name */ -"無名" = "无名"; - -/* Close settings panel */ -"畢" = "毕"; - -/* Time setting: apparent solar time */ -"真太陽時" = "真太阳时"; - -/* Comment: tap to change theme, long press to rename */ -"短按換主題,長按易名" = "短按换主题,长按易名"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - -/* Leap month setting: daily precision */ -"精確至日" = "精确至日"; - -/* Leap month setting: precise */ -"精確至時刻" = "精确至时刻"; - -/* Location - Location View */ -"經緯度" = "经纬度"; - -/* Leap month setting */ -"置閏法" = "置闰法"; - -/* Location not enabled but tried to locate message */ -"蓋因定位未開啓" = "盖因定位未开启"; - -/* Location fails to load */ -"虚無" = "虚无"; - -/* Settings View */ -"設置" = "设置"; - -/* Details about Settings */ -"設置介紹" = "设置更改后自动保存。可调时间、在地、外观等。另有更多有关华历之介绍。"; - -/* Save layout */ -"謄錄" = "謄录"; - -/* Details about Ring Design */ -"輪試設計介紹" = "采用表盘式设计,不同于以往日历形制。一年、一月、一日、一时均周而复始,最适以“轮”代表。呈现细节之外,亦不失大局。"; - -/* Display time - Display Time View */ -"顯示時間" = "显示时间"; - diff --git a/iOS/zh-Hans.lproj/Main.strings b/iOS/zh-Hans.lproj/Main.strings deleted file mode 100644 index 87ffab1..0000000 --- a/iOS/zh-Hans.lproj/Main.strings +++ /dev/null @@ -1,237 +0,0 @@ -/* Class = "UILabel"; text = "日入色"; ObjectID = "0e0-lo-OGc"; */ -"0e0-lo-OGc.text" = "日入色"; - -/* Class = "UILabel"; text = "朔望節氣"; ObjectID = "1HR-Ey-12e"; */ -"1HR-Ey-12e.text" = "朔望节气"; - -/* Class = "UILabel"; text = "小字平移"; ObjectID = "1hu-xy-9gx"; */ -"1hu-xy-9gx.text" = "小字平移"; - -/* Class = "UILabel"; text = "內核色"; ObjectID = "1u3-HL-g6u"; */ -"1u3-HL-g6u.text" = "內核色"; - -/* Class = "UILabel"; text = "圓角比例"; ObjectID = "2U0-7s-zRr"; */ -"2U0-7s-zRr.text" = "圆角比例"; - -/* Class = "UILabel"; text = "日時"; ObjectID = "3FT-tc-vrB"; */ -"3FT-tc-vrB.text" = "日时"; - -/* Class = "UILabel"; text = "小刻透明"; ObjectID = "5bx-eq-Oxv"; */ -"5bx-eq-Oxv.text" = "小刻透明"; - -/* Class = "UILabel"; text = "辰星色"; ObjectID = "5H0-kn-rEg"; */ -"5H0-kn-rEg.text" = "辰星色"; - -/* Class = "UILabel"; text = "1.0"; ObjectID = "5w6-wZ-710"; */ -"5w6-wZ-710.text" = "1.0"; - -/* Class = "UILabel"; text = "大字平移"; ObjectID = "6HB-Mc-8Vy"; */ -"6HB-Mc-8Vy.text" = "大字平移"; - -/* Class = "UILabel"; text = "年圈色"; ObjectID = "6Rp-wy-EDP"; */ -"6Rp-wy-EDP.text" = "年轮色"; - -/* Class = "UILabel"; text = "太白色"; ObjectID = "7Ag-rX-8vz"; */ -"7Ag-rX-8vz.text" = "太白色"; - -/* Class = "UILabel"; text = "明"; ObjectID = "7GV-3Q-HxP"; */ -"7GV-3Q-HxP.text" = "明"; - -/* Class = "UILabel"; text = "氣標記色"; ObjectID = "9Bs-fx-Mqx"; */ -"9Bs-fx-Mqx.text" = "气标记色"; - -/* Class = "UILabel"; text = "殘圈透明"; ObjectID = "9O4-pc-7B4"; */ -"9O4-pc-7B4.text" = "残圈透明"; - -/* Class = "UILabel"; text = "輪式設計"; ObjectID = "46h-V0-3UW"; */ -"46h-V0-3UW.text" = "轮式设计"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "72l-Zb-OQ3"; */ -"72l-Zb-OQ3.text" = "暗"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "89r-Ln-eZg"; */ -"89r-Ln-eZg.text" = "暗"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "93w-GC-vSC"; */ -"93w-GC-vSC.text" = "回环"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "ayK-26-pLM"; */ -"ayK-26-pLM.text" = "暗"; - -/* Class = "UILabel"; text = "大刻透明"; ObjectID = "Btj-qO-PGB"; */ -"Btj-qO-PGB.text" = "大刻透明"; - -/* Class = "UILabel"; text = "鎮星色"; ObjectID = "bXc-NQ-BtY"; */ -"bXc-NQ-BtY.text" = "镇星色"; - -/* Class = "UILabel"; text = "望標記色"; ObjectID = "cN5-cm-WtA"; */ -"cN5-cm-WtA.text" = "望标记色"; - -/* Class = "UILabel"; text = "華曆"; ObjectID = "CpE-Ch-tZF"; */ -"CpE-Ch-tZF.text" = "华历"; - -/* Class = "UILabel"; text = "大字色"; ObjectID = "ctU-7S-kE1"; */ -"ctU-7S-kE1.text" = "大字色"; - -/* Class = "UILabel"; text = "月圈色"; ObjectID = "CZS-0s-tDB"; */ -"CZS-0s-tDB.text" = "月轮色"; - -/* Class = "UINavigationItem"; title = "設置"; ObjectID = "dgF-Pn-a5p"; */ -"dgF-Pn-a5p.title" = "设置"; - -/* Class = "UILabel"; text = "月出入"; ObjectID = "dYQ-sw-ZS7"; */ -"dYQ-sw-ZS7.text" = "月出入"; - -/* Class = "UILabel"; text = "今時"; ObjectID = "eoa-tt-OR9"; */ -"eoa-tt-OR9.text" = "今时"; - -/* Class = "UILabel"; text = "日圈色"; ObjectID = "evD-bV-0Hj"; */ -"evD-bV-0Hj.text" = "日轮色"; - -/* Class = "UITextView"; text = "採用錶盤式設計,不同於以往日曆形制。一年、一月、一日、一時均週而復始,最適以「輪」代表。呈現細節之外,亦不失大局。"; ObjectID = "fRL-Ka-BEX"; */ -"fRL-Ka-BEX.text" = "采用表盘式设计,不同于以往日历形制。一年、一月、一日、一时均周而复始,最适以“轮”代表。呈现细节之外,亦不失大局。"; - -/* Class = "UILabel"; text = "寬"; ObjectID = "gLV-vh-zjy"; */ -"gLV-vh-zjy.text" = "宽"; - -/* Class = "UILabel"; text = "月中色"; ObjectID = "gQD-xr-xf8"; */ -"gQD-xr-xf8.text" = "月中色"; - -/* Class = "UILabel"; text = "大刻色"; ObjectID = "h2X-O3-Q0s"; */ -"h2X-O3-Q0s.text" = "大刻色"; - -/* Class = "UILabel"; text = "朔標記色"; ObjectID = "h92-Li-DOy"; */ -"h92-Li-DOy.text" = "朔标记色"; - -/* Class = "UILabel"; text = "月出色"; ObjectID = "HeB-cG-0Dr"; */ -"HeB-cG-0Dr.text" = "月出色"; - -/* Class = "UILabel"; text = "月入色"; ObjectID = "hfx-at-bvt"; */ -"hfx-at-bvt.text" = "月入色"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "Hk3-HK-Q5L"; */ -"Hk3-HK-Q5L.text" = "回环"; - -/* Class = "UILabel"; text = "五星"; ObjectID = "hs2-Ba-gBd"; */ -"hs2-Ba-gBd.text" = "五星"; - -/* Class = "UILabel"; text = "明"; ObjectID = "iCh-Iu-Fvd"; */ -"iCh-Iu-Fvd.text" = "明"; - -/* Class = "UILabel"; text = "長按錶盤進設置"; ObjectID = "Ika-1B-exq"; */ -"Ika-1B-exq.text" = "长按表盘进设置"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "iL5-OW-xD2"; */ -"iL5-OW-xD2.text" = "暗"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "IXz-vg-R7g"; */ -"IXz-vg-R7g.text" = "暗"; - -/* Class = "UILabel"; text = "熒惑色"; ObjectID = "iZi-62-FRb"; */ -"iZi-62-FRb.text" = "荧惑色"; - -/* Class = "UILabel"; text = "中氣色"; ObjectID = "JoB-wn-GrI"; */ -"JoB-wn-GrI.text" = "中气色"; - -/* Class = "UILabel"; text = "小字色"; ObjectID = "JtQ-6v-rbs"; */ -"JtQ-6v-rbs.text" = "小字色"; - -/* Class = "UILabel"; text = "小字縱移"; ObjectID = "KGD-r8-uTM"; */ -"KGD-r8-uTM.text" = "小字纵移"; - -/* Class = "UILabel"; text = "明"; ObjectID = "KPQ-Y5-gYy"; */ -"KPQ-Y5-gYy.text" = "明"; - -/* Class = "UILabel"; text = "透明度"; ObjectID = "L9j-Kz-U52"; */ -"L9j-Kz-U52.text" = "透明度"; - -/* Class = "UILabel"; text = "明"; ObjectID = "LeU-hh-otX"; */ -"LeU-hh-otX.text" = "明"; - -/* Class = "UISegmentedControl"; LtQ-H1-z6G.segmentTitles[0] = "手錄"; ObjectID = "LtQ-H1-z6G"; */ -"LtQ-H1-z6G.segmentTitles[0]" = "手录"; - -/* Class = "UISegmentedControl"; LtQ-H1-z6G.segmentTitles[1] = "今地"; ObjectID = "LtQ-H1-z6G"; */ -"LtQ-H1-z6G.segmentTitles[1]" = "今地"; - -/* Class = "UILabel"; text = "大字縱移"; ObjectID = "lVa-YZ-exC"; */ -"lVa-YZ-exC.text" = "大字纵移"; - -/* Class = "UILabel"; text = "月色"; ObjectID = "Lwn-br-yo9"; */ -"Lwn-br-yo9.text" = "月色"; - -/* Class = "UILabel"; text = "在地"; ObjectID = "m39-NT-rXb"; */ -"m39-NT-rXb.text" = "在地"; - -/* Class = "UILabel"; text = "0.25"; ObjectID = "Mto-Hp-LF8"; */ -"Mto-Hp-LF8.text" = "0.25"; - -/* Class = "UILabel"; text = "日出色"; ObjectID = "mtt-kV-gTE"; */ -"mtt-kV-gTE.text" = "日出色"; - -/* Class = "UILabel"; text = "高"; ObjectID = "NMv-Vw-KIV"; */ -"NMv-Vw-KIV.text" = "高"; - -/* Class = "UILabel"; text = "經度"; ObjectID = "NR0-7n-bua"; */ -"NR0-7n-bua.text" = "经度"; - -/* Class = "UILabel"; text = "時區"; ObjectID = "oAy-Fp-oEE"; */ -"oAy-Fp-oEE.text" = "时区"; - -/* Class = "UILabel"; text = "迴環"; ObjectID = "oxC-5l-WpM"; */ -"oxC-5l-WpM.text" = "回环"; - -/* Class = "UILabel"; text = "明"; ObjectID = "PHO-EE-lP5"; */ -"PHO-EE-lP5.text" = "明"; - -/* Class = "UILabel"; text = "經緯度"; ObjectID = "pl1-Ib-blg"; */ -"pl1-Ib-blg.text" = "经纬度"; - -/* Class = "UILabel"; text = "明"; ObjectID = "pUm-7e-ynh"; */ -"pUm-7e-ynh.text" = "明"; - -/* Class = "UILabel"; text = "日出入"; ObjectID = "qiK-yl-54u"; */ -"qiK-yl-54u.text" = "日出入"; - -/* Class = "UILabel"; text = "漸變色"; ObjectID = "RuP-Yw-XoK"; */ -"RuP-Yw-XoK.text" = "渐变色"; - -/* Class = "UILabel"; text = "夜中色"; ObjectID = "slb-Jl-hOj"; */ -"slb-Jl-hOj.text" = "夜中色"; - -/* Class = "UILabel"; text = "明暗主題色"; ObjectID = "T90-0P-AQ1"; */ -"T90-0P-AQ1.text" = "明暗主题色"; - -/* Class = "UILabel"; text = "暗"; ObjectID = "u6s-lC-FRG"; */ -"u6s-lC-FRG.text" = "暗"; - -/* Class = "UIButton"; configuration.title = "閱"; ObjectID = "UgU-NM-Xad"; */ -"UgU-NM-Xad.configuration.title" = "阅"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "UgU-NM-Xad"; */ -"UgU-NM-Xad.normalTitle" = "Button"; - -/* Class = "UILabel"; text = "歲星色"; ObjectID = "v5B-62-oMP"; */ -"v5B-62-oMP.text" = "岁星色"; - -/* Class = "UILabel"; text = "緯度"; ObjectID = "VJ5-0u-4yr"; */ -"VJ5-0u-4yr.text" = "纬度"; - -/* Class = "UILabel"; text = "節標記色"; ObjectID = "Xbw-rN-8bt"; */ -"Xbw-rN-8bt.text" = "节标记色"; - -/* Class = "UILabel"; text = "1.0"; ObjectID = "xhf-f2-zsv"; */ -"xhf-f2-zsv.text" = "1.0"; - -/* Class = "UITextView"; text = "設置更改後自動保存。可調時間、在地、外觀等。另有更多有關華曆之介紹。"; ObjectID = "zei-WM-ULJ"; */ -"zei-WM-ULJ.text" = "设置更改后自动保存。可调时间、在地、外观等。另有更多有关华历之介绍。"; - -/* Class = "UILabel"; text = "節色"; ObjectID = "ZHY-yC-gqU"; */ -"ZHY-yC-gqU.text" = "节色"; - -/* Class = "UILabel"; text = "日中色"; ObjectID = "zij-j0-s08"; */ -"zij-j0-s08.text" = "日中色"; - -/* Class = "UILabel"; text = "小刻色"; ObjectID = "ZNL-AU-VER"; */ -"ZNL-AU-VER.text" = "小刻色"; - diff --git a/iOS/zh-Hant.lproj/InfoPlist.strings b/iOS/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 5bec380..0000000 --- a/iOS/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "華曆"; - -/* Bundle name */ -"CFBundleName" = "華曆"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - diff --git a/iOS/zh-Hant.lproj/Localizable.strings b/iOS/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 73960bf..0000000 --- a/iOS/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,20 +0,0 @@ -/* Default save file name */ -"Default" = "常備"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# 爲何要做此華曆?\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**五星**之古名與今不同。辰星即今之水星,又單名「星」;太白星即今之金星,以其爲天空最亮之星,故名太白;熒惑即今之火星;歲星即今之木星,太歲乃與歲星相關但不同的概念;填星又作鎮星,即今之土星。"; - -/* Circle Color View - Circle colors */ -"圈色" = "輪色"; - -/* Mark Color View - Mark colors */ -"塊標色" = "色塊"; - -/* Details about Settings */ -"設置介紹" = "設置更改後自動保存。可調時間、在地、外觀等。另有更多有關華曆之介紹。"; - -/* Details about Ring Design */ -"輪試設計介紹" = "採用錶盤式設計,不同於以往日曆形制。一年、一月、一日、一時均週而復始,最適以「輪」代表。呈現細節之外,亦不失大局。"; - diff --git a/iOS/zh-Hant.lproj/Main.strings b/iOS/zh-Hant.lproj/Main.strings deleted file mode 100644 index a458bbf..0000000 --- a/iOS/zh-Hant.lproj/Main.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Class = "UILabel"; text = "年圈色"; ObjectID = "6Rp-wy-EDP"; */ -"6Rp-wy-EDP.text" = "年輪色"; - -/* Class = "UILabel"; text = "月圈色"; ObjectID = "CZS-0s-tDB"; */ -"CZS-0s-tDB.text" = "月輪色"; - -/* Class = "UILabel"; text = "日圈色"; ObjectID = "evD-bV-0Hj"; */ -"evD-bV-0Hj.text" = "日輪色"; - -/* Class = "UILabel"; text = "子夜色"; ObjectID = "slb-Jl-hOj"; */ -"slb-Jl-hOj.text" = "夜中色"; - diff --git a/iOSWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOSWidget/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3..0000000 --- a/iOSWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOSWidget/Assets.xcassets/Contents.json b/iOSWidget/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/iOSWidget/Assets.xcassets/Contents.json +++ b/iOSWidget/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/iOSWidget/Info.plist b/iOSWidget/Info.plist index 2092828..27799cf 100644 --- a/iOSWidget/Info.plist +++ b/iOSWidget/Info.plist @@ -4,6 +4,8 @@ ITSAppUsesNonExemptEncryption + LSHasLocalizedDisplayName + NSExtension NSExtensionPointIdentifier diff --git a/iOSWidget/en.lproj/InfoPlist.strings b/iOSWidget/en.lproj/InfoPlist.strings deleted file mode 100644 index 55fdeca..0000000 --- a/iOSWidget/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "Chinese Time Widget"; - -/* Bundle name */ -"CFBundleName" = "iOS Widget Extension"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/iOSWidget/en.lproj/Localizable.strings b/iOSWidget/en.lproj/Localizable.strings deleted file mode 100644 index 6330088..0000000 --- a/iOSWidget/en.lproj/Localizable.strings +++ /dev/null @@ -1,45 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "Circular"; - -/* No comment provided by engineer. */ -"Circular View." = "You can choose to display sunrise/set, moonrise/set time, or month and day in month"; - -/* No comment provided by engineer. */ -"Compact" = "Compact"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "Compact watch face to display either Date or Time."; - -/* Default save file name */ -"Default" = "Default"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "Display both Date and Time as separate watches, whose order is at your choice."; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "Display full information with both Date and Time."; - -/* No comment provided by engineer. */ -"Dual" = "Dual"; - -/* No comment provided by engineer. */ -"Full" = "Complete"; - -/* No comment provided by engineer. */ -"Single Line" = "Single Line"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "Single line to display date and hour"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "Date and Hour String"; - -/* No comment provided by engineer. */ -"日月光華" = "Sunlight & Moonlight"; - -/* No comment provided by engineer. */ -"歲月之輪" = "Month and Day Rings"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - diff --git a/iOSWidget/iOSWidgetExtension.entitlements b/iOSWidget/iOSWidget.entitlements similarity index 100% rename from iOSWidget/iOSWidgetExtension.entitlements rename to iOSWidget/iOSWidget.entitlements diff --git a/iOSWidget/iOSWidgetBundle.swift b/iOSWidget/iOSWidgetBundle.swift index 4f6fe53..3ac17e6 100644 --- a/iOSWidget/iOSWidgetBundle.swift +++ b/iOSWidget/iOSWidgetBundle.swift @@ -9,17 +9,14 @@ import SwiftUI @main struct iOSWidgetBundle: WidgetBundle { - init() { - DataContainer.shared.loadSave() - LocationManager.shared.requestLocation(completion: nil) - } - - @WidgetBundleBuilder + var body: some Widget { SmallWidget() MediumWidget() LargeWidget() LineWidget() CircularWidget() + RectWidget() + DateCardWidget() } } diff --git a/iOSWidget/zh-Hans.lproj/InfoPlist.strings b/iOSWidget/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index fca942b..0000000 --- a/iOSWidget/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,18 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "华历挂件"; - -/* Bundle name */ -"CFBundleName" = "手机挂件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/iOSWidget/zh-Hans.lproj/Localizable.strings b/iOSWidget/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index b476dec..0000000 --- a/iOSWidget/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,45 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "圆轮"; - -/* No comment provided by engineer. */ -"Circular View." = "可选“日月光华”或“岁月之轮”"; - -/* No comment provided by engineer. */ -"Compact" = "半"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "日、时二择一"; - -/* Default save file name */ -"Default" = "常备"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "日、时分列,顺序可选"; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "完整展示"; - -/* No comment provided by engineer. */ -"Dual" = "双"; - -/* No comment provided by engineer. */ -"Full" = "完"; - -/* No comment provided by engineer. */ -"Single Line" = "纯文字"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "单栏文字展示日、时"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "日、时文字"; - -/* No comment provided by engineer. */ -"日月光華" = "日月光华"; - -/* No comment provided by engineer. */ -"歲月之輪" = "岁月之轮"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - diff --git a/iOSWidget/zh-Hant.lproj/InfoPlist.strings b/iOSWidget/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 884e0db..0000000 --- a/iOSWidget/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Bundle display name */ -"CFBundleDisplayName" = "華曆掛件"; - -/* Bundle name */ -"CFBundleName" = "手機掛件"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - diff --git a/iOSWidget/zh-Hant.lproj/Localizable.strings b/iOSWidget/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index 516e990..0000000 --- a/iOSWidget/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,36 +0,0 @@ -/* No comment provided by engineer. */ -"Circular" = "圓輪"; - -/* No comment provided by engineer. */ -"Circular View." = "可選「日月光華」或「歲月之輪」"; - -/* No comment provided by engineer. */ -"Compact" = "半"; - -/* No comment provided by engineer. */ -"Compact watch face to display either Date or Time." = "日、時二擇一"; - -/* Default save file name */ -"Default" = "常備"; - -/* No comment provided by engineer. */ -"Display both Date and Time as separate watches, whose order is at your choice." = "日、時分列,順序可選"; - -/* No comment provided by engineer. */ -"Display full information with both Date and Time." = "完整展示"; - -/* No comment provided by engineer. */ -"Dual" = "雙"; - -/* No comment provided by engineer. */ -"Full" = "完"; - -/* No comment provided by engineer. */ -"Single Line" = "純文字"; - -/* No comment provided by engineer. */ -"Single line date and time widget." = "單欄文字展示日、時"; - -/* No comment provided by engineer. */ -"Single Line Widget" = "日、時文字"; - diff --git a/macOS/AppDelegate.swift b/macOS/AppDelegate.swift deleted file mode 100644 index 1f09a6f..0000000 --- a/macOS/AppDelegate.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AppDelegate.swift -// ChineseTime -// -// Created by LEO Yoon-Tsaw on 9/19/21. -// - -import Cocoa -import MapKit -import WidgetKit - -var statusItem: NSStatusItem? -func updateStatusTitle(title: String) { - if let button = statusItem?.button { - button.title = String(title.reversed()) - statusItem?.length = button.intrinsicContentSize.width - } -} - -func updatePosition() { - if let frame = statusItem?.button?.window?.frame { - WatchFace.currentInstance?.moveTopCenter(to: NSMakePoint(NSMidX(frame), NSMinY(frame))) - } -} - -@main -final class AppDelegate: NSObject, NSApplicationDelegate { - func applicationWillFinishLaunching(_ aNotification: Notification) { - statusItem = NSStatusBar.system.statusItem(withLength: 0) - statusItem?.button?.action = #selector(self.toggleDisplay(sender:)) - statusItem?.button?.sendAction(on: [.leftMouseDown]) - } - - @objc func toggleDisplay(sender: NSStatusItem) { - if let watchFace = WatchFace.currentInstance { - if watchFace.isVisible { - WidgetCenter.shared.reloadAllTimelines() - watchFace.hide() - } else { - LocationManager.shared.requestLocation { _ in - watchFace._view.drawView(forceRefresh: true) - } - watchFace.show() - updatePosition() - NSApp.activate(ignoringOtherApps: true) - } - } - } - - func applicationDidResignActive(_ notification: Notification) { - WatchFace.currentInstance?.hide() - } - - func applicationDidFinishLaunching(_ aNotification: Notification) { - DataContainer.shared.loadSave() - let preview = WatchFace(position: NSZeroRect) - LocationManager.shared.requestLocation { _ in - WatchFace.currentInstance?._view.drawView(forceRefresh: true) - } - preview.show() - NSApp.activate(ignoringOtherApps: true) - } - - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } - - // MARK: - Core Data Saving and Undo support - - func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? { - // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. - return DataContainer.shared.persistentContainer.viewContext.undoManager - } - - func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - // Save changes in the application's managed object context before the application terminates. - let context = DataContainer.shared.persistentContainer.viewContext - - if !context.commitEditing() { - NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate") - return .terminateCancel - } - - if !context.hasChanges { - return .terminateNow - } - - do { - try context.save() - } catch { - let nserror = error as NSError - - // Customize this code block to include application-specific recovery steps. - let result = sender.presentError(nserror) - if result { - return .terminateCancel - } - - let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message") - let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info") - let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title") - let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title") - let alert = NSAlert() - alert.messageText = question - alert.informativeText = info - alert.addButton(withTitle: quitButton) - alert.addButton(withTitle: cancelButton) - alert.alertStyle = .critical - let answer = alert.runModal() - if answer == .alertSecondButtonReturn { - return .terminateCancel - } - } - // If we got here, it is time to quit. - return .terminateNow - } -} diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json index d14bcc4..e3cfbfe 100644 --- a/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { "images" : [ { - "filename" : "mac 16.png", + "size" : "16x16", "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" + "filename" : "mac 16.png", + "scale" : "1x" }, { - "filename" : "mac 33.png", + "size" : "16x16", "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" + "filename" : "mac 32.png", + "scale" : "2x" }, { - "filename" : "mac 32.png", + "size" : "32x32", "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" + "filename" : "mac 32.png", + "scale" : "1x" }, { - "filename" : "mac 64.png", + "size" : "32x32", "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" + "filename" : "mac 64.png", + "scale" : "2x" }, { - "filename" : "mac 128.png", + "size" : "128x128", "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" + "filename" : "mac 128.png", + "scale" : "1x" }, { - "filename" : "mac 257.png", + "size" : "128x128", "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" + "filename" : "mac 256.png", + "scale" : "2x" }, { - "filename" : "mac 256.png", + "size" : "256x256", "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" + "filename" : "mac 256.png", + "scale" : "1x" }, { - "filename" : "mac512 1.png", + "size" : "256x256", "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" + "filename" : "mac 512.png", + "scale" : "2x" }, { - "filename" : "mac512.png", + "size" : "512x512", "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" + "filename" : "mac 512.png", + "scale" : "1x" }, { - "filename" : "mac1024.png", + "size" : "512x512", "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "filename" : "mac 1024.png", + "scale" : "2x" } ], "info" : { - "author" : "xcode", - "version" : 1 + "version" : 1, + "author" : "xcode" } } diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 1024.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 1024.png new file mode 100644 index 0000000..347287a Binary files /dev/null and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 1024.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 128.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 128.png index c3bafdb..50a7f24 100644 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 128.png and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 128.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 16.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 16.png index 241699b..5f0f329 100644 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 16.png and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 16.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 256.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 256.png index 9ecd187..209a0da 100644 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 256.png and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 256.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 257.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 257.png deleted file mode 100644 index 9ecd187..0000000 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 257.png and /dev/null differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 32.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 32.png index a7f8366..ceb3fd2 100644 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 32.png and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 32.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 33.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 33.png deleted file mode 100644 index a7f8366..0000000 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 33.png and /dev/null differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 512.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 512.png new file mode 100644 index 0000000..c109a60 Binary files /dev/null and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 512.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac 64.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac 64.png index 65af7b0..77aa012 100644 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac 64.png and b/macOS/Assets.xcassets/AppIcon.appiconset/mac 64.png differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac1024.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac1024.png deleted file mode 100644 index 4b39b41..0000000 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac1024.png and /dev/null differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac512 1.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac512 1.png deleted file mode 100644 index bac904b..0000000 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac512 1.png and /dev/null differ diff --git a/macOS/Assets.xcassets/AppIcon.appiconset/mac512.png b/macOS/Assets.xcassets/AppIcon.appiconset/mac512.png deleted file mode 100644 index bac904b..0000000 Binary files a/macOS/Assets.xcassets/AppIcon.appiconset/mac512.png and /dev/null differ diff --git a/macOS/Assets.xcassets/Contents.json b/macOS/Assets.xcassets/Contents.json index 73c0059..8cbf8bf 100644 --- a/macOS/Assets.xcassets/Contents.json +++ b/macOS/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "gpu-optimized-best" } } diff --git a/macOS/Assets.xcassets/Image.imageset/Contents.json b/macOS/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 0000000..273cf1b --- /dev/null +++ b/macOS/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "icon 120.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon 240.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon 360.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macOS/Assets.xcassets/Image.imageset/icon 120.png b/macOS/Assets.xcassets/Image.imageset/icon 120.png new file mode 100644 index 0000000..8b0eda0 Binary files /dev/null and b/macOS/Assets.xcassets/Image.imageset/icon 120.png differ diff --git a/macOS/Assets.xcassets/Image.imageset/icon 240.png b/macOS/Assets.xcassets/Image.imageset/icon 240.png new file mode 100644 index 0000000..279a007 Binary files /dev/null and b/macOS/Assets.xcassets/Image.imageset/icon 240.png differ diff --git a/macOS/Assets.xcassets/Image.imageset/icon 360.png b/macOS/Assets.xcassets/Image.imageset/icon 360.png new file mode 100644 index 0000000..5c14859 Binary files /dev/null and b/macOS/Assets.xcassets/Image.imageset/icon 360.png differ diff --git a/macOS/Base.lproj/Main.storyboard b/macOS/Base.lproj/Main.storyboard deleted file mode 100644 index 38d6694..0000000 --- a/macOS/Base.lproj/Main.storyboard +++ /dev/null @@ -1,1599 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macOS/ChineseTime.entitlements b/macOS/ChineseTimeMac.entitlements similarity index 100% rename from macOS/ChineseTime.entitlements rename to macOS/ChineseTimeMac.entitlements diff --git a/macOS/Info.plist b/macOS/Info.plist index c6b0dc0..ef79656 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -6,6 +6,8 @@ . CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile @@ -15,7 +17,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - Chinese Time + $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString @@ -28,6 +30,8 @@ LSApplicationCategoryType public.app-category.utilities + LSHasLocalizedDisplayName + LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement @@ -35,13 +39,11 @@ NSHumanReadableCopyright Open source under GPL v3 NSLocationAlwaysAndWhenInUseUsageDescription - 提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。 + Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse. NSLocationUsageDescription - 提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。 + Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse. NSLocationWhenInUseUsageDescription - 提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。 - NSMainStoryboardFile - Main + Provide location to calculate local sunrise/set, moonrise/set times. No impact on other functions if you refuse. NSPrincipalClass NSApplication NSUserActivityTypes diff --git a/macOS/Layout.swift b/macOS/Layout.swift index 5a65b03..9c4a33a 100644 --- a/macOS/Layout.swift +++ b/macOS/Layout.swift @@ -5,24 +5,72 @@ // Created by LEO Yoon-Tsaw on 9/23/21. // -import AppKit +import SwiftUI +import Observation -final class WatchLayout: MetaWatchLayout { - static var shared: WatchLayout = .init() +@Observable final class WatchLayout: MetaWatchLayout { + static var shared = WatchLayout() + + struct StatusBar: Equatable { + + enum Separator: String, CaseIterable { + case space, dot, none + var symbol: String { + switch self { + case .space: " " + case .dot: "・" + case .none: "" + } + } + } + + var date: Bool + var time: Bool + var holiday: Int + var separator: Separator + + func encode() -> String { + "date: \(date.description), time: \(time.description), holiday: \(holiday.description), separator: \(separator.rawValue)" + } + + init(date: Bool = true, time: Bool = true, holiday: Int = 0, separator: Separator = .space) { + self.date = date + self.time = time + self.holiday = holiday + self.separator = separator + } + + init?(from str: String?) { + guard let str = str else { return nil } + let regex = /([a-zA-Z_0-9]+)\s*:[\s"]*([^\s"#][^"#]*)[\s"#]*(#*.*)$/ + var values = [String: String]() + for line in str.split(separator: ",") { + if let match = try? regex.firstMatch(in: String(line))?.output { + values[String(match.1)] = String(match.2) + } + } + guard let date = values["date"]?.boolValue, let time = values["time"]?.boolValue, let holiday = values["holiday"]?.intValue, let separator = values["separator"] else { return nil } + self.date = date + self.time = time + self.holiday = max(0, min(2, holiday)) + self.separator = Separator(rawValue: separator) ?? .space + } + } - var textFont: NSFont - var centerFont: NSFont - override init() { - textFont = NSFont.userFont(ofSize: NSFont.systemFontSize)! - centerFont = NSFontManager.shared.font(withFamily: NSFont.userFont(ofSize: NSFont.systemFontSize)!.familyName!, - traits: .boldFontMask, weight: 900, size: NSFont.systemFontSize)! + var textFont: NSFont = NSFont.userFont(ofSize: NSFont.systemFontSize)! + var centerFont: NSFont = NSFontManager.shared.font(withFamily: NSFont.userFont(ofSize: NSFont.systemFontSize)!.familyName!, + traits: .boldFontMask, weight: 900, size: NSFont.systemFontSize)! + var statusBar = StatusBar() + + private override init() { super.init() } - override func encode(includeOffset: Bool = true) -> String { - var encoded = super.encode() + override func encode(includeOffset: Bool = true, includeColor: Bool = true, includeConfig: Bool = true) -> String { + var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor, includeConfig: includeConfig) encoded += "textFont: \(textFont.fontName)\n" encoded += "centerFont: \(centerFont.fontName)\n" + encoded += "statusBar: \(statusBar.encode())\n" return encoded } @@ -34,5 +82,31 @@ final class WatchLayout: MetaWatchLayout { if let name = values["centerFont"] { centerFont = NSFont(name: name, size: NSFont.systemFontSize) ?? centerFont } + if let value = values["statusBar"] { + statusBar = StatusBar(from: value) ?? statusBar + } + } + + var monochrome: Self { + let emptyLayout = Self.init() + emptyLayout.update(from: self.encode(includeColor: false)) + return emptyLayout + } + + func binding(_ keyPath: ReferenceWritableKeyPath) -> Binding { + return Binding(get: { self[keyPath: keyPath] }, set: { self[keyPath: keyPath] = $0 }) + } +} + +@Observable class WatchSetting { + static let shared = WatchSetting() + enum Selection: String, CaseIterable { + case datetime, location, ringColor, markColor, layout, themes, documentation } + + var displayTime: Date? = nil + var timezone: TimeZone? = nil + @ObservationIgnored var previousSelection: Selection? = nil + + private init() {} } diff --git a/macOS/ViewController.swift b/macOS/ViewController.swift deleted file mode 100644 index 541e175..0000000 --- a/macOS/ViewController.swift +++ /dev/null @@ -1,1267 +0,0 @@ -// -// ViewController.swift -// Chinese Time -// -// Created by Leo Liu on 4/29/23. -// - -import AppKit - -final class ColorWell: NSColorWell { - override func mouseDown(with event: NSEvent) { - window?.makeFirstResponder(self) - NSColorPanel.shared.showsAlpha = true - super.mouseDown(with: event) - } - - override func resignFirstResponder() -> Bool { - NSColorPanel.shared.close() - return super.resignFirstResponder() - } -} - -final class GradientSlider: NSControl, NSColorChanging { - let minimumValue: CGFloat = 0 - let maximumValue: CGFloat = 1 - var values: [CGFloat] = [0, 1] - var colors: [NSColor] = [.black, .white] - private var controls = [CAShapeLayer]() - private var controlsLayer = CALayer() - - var isLoop = false - private let trackLayer = CAGradientLayer() - private var previousLocation: CGPoint? = nil - private var movingControl: CAShapeLayer? = nil - private var movingIndex: Int? = nil - private var controlRadius: CGFloat = 0 - private var dragging = false - - var gradient: WatchLayout.Gradient { - get { - return WatchLayout.Gradient(locations: values, colors: colors.map { $0.cgColor }, loop: isLoop) - } set { - if newValue.isLoop { - values = newValue.locations.dropLast() - colors = newValue.colors.dropLast().map { NSColor(cgColor: $0)! } - } else { - values = newValue.locations - colors = newValue.colors.map { NSColor(cgColor: $0)! } - } - isLoop = newValue.isLoop - updateLayerFrames() - initializeControls() - updateGradient() - } - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - self.wantsLayer = true - layer?.addSublayer(trackLayer) - layer?.addSublayer(controlsLayer) - updateLayerFrames() - initializeControls() - updateGradient() - } - - override var frame: CGRect { - didSet { - updateLayerFrames() - changeChontrols() - } - } - - override var acceptsFirstResponder: Bool { - return true - } - - private func addControl(at value: CGFloat, color: NSColor) { - let control = CAShapeLayer() - control.path = CGPath(ellipseIn: NSRect(origin: thumbOriginForValue(value), size: NSMakeSize(controlRadius * 2, controlRadius * 2)), transform: nil) - control.fillColor = color.cgColor - control.shadowPath = control.path - control.shadowOpacity = 0.3 - control.shadowRadius = 1.5 - control.shadowOffset = NSMakeSize(0, -1) - control.lineWidth = 2.0 - controls.append(control) - controlsLayer.addSublayer(control) - } - - private func initializeControls() { - controlsLayer.sublayers = [] - controls = [] - for i in 0.. CGFloat { - return trackLayer.frame.width * value - } - - private func thumbOriginForValue(_ value: CGFloat) -> CGPoint { - let x = positionForValue(value) - controlRadius - return CGPoint(x: trackLayer.frame.minX + x, y: bounds.height / 2 - controlRadius) - } - - override func resignFirstResponder() -> Bool { - movingControl?.strokeColor = nil - movingControl = nil - movingIndex = nil - previousLocation = nil - return true - } - - override func mouseDown(with event: NSEvent) { - window?.makeFirstResponder(self) - previousLocation = event.locationInWindow - previousLocation = convert(previousLocation!, from: window?.contentView) - var hit = false - var i = 0 - for control in controls { - if control.path!.contains(previousLocation!) { - movingControl?.strokeColor = nil - movingControl = control - movingControl!.strokeColor = NSColor.controlAccentColor.cgColor - movingIndex = i - hit = true - } - i += 1 - } - if !hit { - var newValue = (previousLocation!.x - trackLayer.frame.minX) / trackLayer.frame.width - newValue = min(max(newValue, minimumValue), maximumValue) - let color = gradient.interpolate(at: newValue) - values.append(newValue) - colors.append(NSColor(cgColor: color)!) - addControl(at: newValue, color: NSColor(cgColor: color)!) - movingIndex = values.count - 1 - movingControl?.strokeColor = nil - movingControl = controls.last! - movingControl!.strokeColor = NSColor.controlAccentColor.cgColor - } - } - - override func mouseDragged(with event: NSEvent) { - dragging = true - if (movingIndex != nil) && (previousLocation != nil) && (movingIndex! < values.count) { - var location = event.locationInWindow - location = convert(location, from: window?.contentView) - let deltaLocation = location.x - previousLocation!.x - let deltaValue = (maximumValue - minimumValue) * deltaLocation / trackLayer.frame.width - previousLocation = location - // Change value - values[movingIndex!] += deltaValue - values[movingIndex!] = min(max(values[movingIndex!], minimumValue), maximumValue) - moveControl() - } - } - - override func mouseUp(with event: NSEvent) { - if !dragging && movingControl != nil && movingIndex != nil { - NSColorPanel.shared.showsAlpha = true - let colorPicker = NSColorPanel.shared - if let currentColor = movingControl?.fillColor { - colorPicker.color = NSColor(cgColor: currentColor)! - colorPicker.orderFront(self) - colorPicker.setTarget(self) - colorPicker.setAction(#selector(changeColor(_:))) - } - } - dragging = false - previousLocation = nil - } - - override func keyUp(with event: NSEvent) { - if event.keyCode == 51 && movingControl != nil && movingIndex != nil && values.count > 2 && colors.count > 2 { - movingControl?.strokeColor = nil - movingControl!.removeFromSuperlayer() - controls.remove(at: movingIndex!) - values.remove(at: movingIndex!) - colors.remove(at: movingIndex!) - movingControl = nil - movingIndex = nil - NSColorPanel.shared.close() - updateGradient() - } - } - - override func performKeyEquivalent(with event: NSEvent) -> Bool { - if event.keyCode == 51 && movingControl != nil && movingIndex != nil && values.count > 2 && colors.count > 2 { - return true - } else { - return false - } - } - - @objc func changeColor(_ sender: NSColorPanel?) { - if movingControl != nil && movingIndex != nil && sender != nil { - movingControl!.fillColor = sender!.color.cgColor - colors[movingIndex!] = sender!.color - updateGradient() - } - } -} - -final class ConfigurationViewController: NSViewController, NSWindowDelegate { - static var currentInstance: ConfigurationViewController? = nil - @IBOutlet var clearLocationButton: NSButton! - @IBOutlet var globalMonthPicker: NSPopUpButton! - @IBOutlet var apparentTimePicker: NSPopUpButton! - @IBOutlet var datetimePicker: NSDatePicker! - @IBOutlet var currentTimeToggle: NSButton! - @IBOutlet var timezonePicker: NSPopUpButton! - @IBOutlet var latitudeDegreePicker: NSTextField! - @IBOutlet var latitudeMinutePicker: NSTextField! - @IBOutlet var latitudeSecondPicker: NSTextField! - @IBOutlet var latitudeSpherePicker: NSSegmentedControl! - @IBOutlet var longitudeDegreePicker: NSTextField! - @IBOutlet var longitudeMinutePicker: NSTextField! - @IBOutlet var longitudeSecondPicker: NSTextField! - @IBOutlet var longitudeSpherePicker: NSSegmentedControl! - @IBOutlet var currentLocationToggle: NSButton! - @IBOutlet var firstRingGradientPicker: GradientSlider! - @IBOutlet var secondRingGradientPicker: GradientSlider! - @IBOutlet var thirdRingGradientPicker: GradientSlider! - @IBOutlet var shadeAlphaValuePicker: NSSlider! - @IBOutlet var shadeAlphaValueLabel: NSTextField! - @IBOutlet var firstRingIsLoop: NSButton! - @IBOutlet var secondRingIsLoop: NSButton! - @IBOutlet var thirdRingIsLoop: NSButton! - @IBOutlet var innerTextGradientPicker: GradientSlider! - @IBOutlet var minorTickAlphaValuePicker: NSSlider! - @IBOutlet var minorTickAlphaValueLabel: NSTextField! - @IBOutlet var majorTickAlphaValuePicker: NSSlider! - @IBOutlet var majorTickAlphaValueLabel: NSTextField! - @IBOutlet var innerColorPicker: NSColorWell! - @IBOutlet var majorTickColorPicker: NSColorWell! - @IBOutlet var minorTickColorPicker: NSColorWell! - @IBOutlet var textColorPicker: NSColorWell! - @IBOutlet var oddStermTickColorPicker: NSColorWell! - @IBOutlet var evenStermTickColorPicker: NSColorWell! - @IBOutlet var innerColorPickerDark: NSColorWell! - @IBOutlet var majorTickColorPickerDark: NSColorWell! - @IBOutlet var minorTickColorPickerDark: NSColorWell! - @IBOutlet var textColorPickerDark: NSColorWell! - @IBOutlet var oddStermTickColorPickerDark: NSColorWell! - @IBOutlet var evenStermTickColorPickerDark: NSColorWell! - @IBOutlet var mercuryIndicatorColorPicker: NSColorWell! - @IBOutlet var venusIndicatorColorPicker: NSColorWell! - @IBOutlet var marsIndicatorColorPicker: NSColorWell! - @IBOutlet var jupyterIndicatorColorPicker: NSColorWell! - @IBOutlet var saturnIndicatorColorPicker: NSColorWell! - @IBOutlet var moonIndicatorColorPicker: NSColorWell! - @IBOutlet var eclipseIndicatorColorPicker: NSColorWell! - @IBOutlet var fullmoonIndicatorColorPicker: NSColorWell! - @IBOutlet var oddStermIndicatorColorPicker: NSColorWell! - @IBOutlet var evenStermIndicatorColorPicker: NSColorWell! - @IBOutlet var sunriseIndicatorColorPicker: NSColorWell! - @IBOutlet var sunsetIndicatorColorPicker: NSColorWell! - @IBOutlet var noonIndicatorColorPicker: NSColorWell! - @IBOutlet var midnightIndicatorColorPicker: NSColorWell! - @IBOutlet var moonriseIndicatorColorPicker: NSColorWell! - @IBOutlet var moonsetIndicatorColorPicker: NSColorWell! - @IBOutlet var moonmeridianIndicatorColorPicker: NSColorWell! - @IBOutlet var textFontFamilyPicker: NSPopUpButton! - @IBOutlet var textFontTraitPicker: NSPopUpButton! - @IBOutlet var centerTextFontFamilyPicker: NSPopUpButton! - @IBOutlet var centerTextFontTraitPicker: NSPopUpButton! - @IBOutlet var widthPicker: NSTextField! - @IBOutlet var heightPicker: NSTextField! - @IBOutlet var cornerRadiusRatioPicker: NSTextField! - @IBOutlet var centerTextOffsetPicker: NSTextField! - @IBOutlet var centerTextHOffsetPicker: NSTextField! - @IBOutlet var textHorizontalOffsetPicker: NSTextField! - @IBOutlet var textVerticalOffsetPicker: NSTextField! - @IBOutlet var doneButton: NSButton! - @IBOutlet var themesButton: NSButton! - @IBOutlet var applyButton: NSButton! - @IBOutlet var scrollView: NSScrollView! - @IBOutlet var contentView: NSView! - - var panelTimezone = TimeZone(secondsFromGMT: 0)! - - func scrollToTop() { - let maxHeight = max(scrollView.bounds.height, contentView.bounds.height) - contentView.scroll(NSPoint(x: 0, y: maxHeight)) - } - - func toggle(button: NSButton, with bool: Bool) { - if bool { - button.state = .on - } else { - button.state = .off - } - } - - func readToggle(button: NSButton) -> Bool { - return button.state == .on - } - - func populateFontMember(_ picker: NSPopUpButton, inFamily familyPicker: NSPopUpButton) { - picker.removeAllItems() - if let family = familyPicker.titleOfSelectedItem { - let members = NSFontManager.shared.availableMembers(ofFontFamily: family) - for member in members ?? [[Any]]() { - if let fontType = member[1] as? String { - picker.addItem(withTitle: fontType) - } - } - } - picker.selectItem(at: 0) - } - - func populateFontFamilies(_ picker: NSPopUpButton) { - picker.removeAllItems() - picker.addItem(withTitle: NSFont.systemFont(ofSize: NSFont.systemFontSize).familyName!) - picker.addItems(withTitles: NSFontManager.shared.availableFontFamilies) - } - - func readFont(family: NSPopUpButton, style: NSPopUpButton) -> NSFont? { - let size = NSFont.systemFontSize - let fontFamily: String = family.titleOfSelectedItem! - let fontTraits: String = style.titleOfSelectedItem! - if let font = NSFont(name: "\(fontFamily.filter { !$0.isWhitespace })-\(fontTraits.filter { !$0.isWhitespace })", size: size) { - return font - } - let members = NSFontManager.shared.availableMembers(ofFontFamily: fontFamily) ?? [[Any]]() - for i in 0..= 0 ? 0 : 1 - latitude = abs(latitude) - latitudeDegreePicker.doubleValue = floor(latitude) - latitude = (latitude - floor(latitude)) * 60 - latitudeMinutePicker.doubleValue = floor(latitude) - latitude = (latitude - floor(latitude)) * 60 - latitudeSecondPicker.doubleValue = latitude - - var longitude = location.y - longitudeSpherePicker.selectedSegment = longitude >= 0 ? 0 : 1 - longitude = abs(longitude) - longitudeDegreePicker.doubleValue = floor(longitude) - longitude = (longitude - floor(longitude)) * 60 - longitudeMinutePicker.doubleValue = floor(longitude) - longitude = (longitude - floor(longitude)) * 60 - longitudeSecondPicker.doubleValue = longitude - - if LocationManager.shared.location != nil { - currentLocationToggle.state = .on - } else { - currentLocationToggle.state = .off - } - - apparentTimePicker.isEnabled = true - apparentTimePicker.selectItem(at: ChineseCalendar.apparentTime ? 1 : 0) - } else { - apparentTimePicker.isEnabled = false - apparentTimePicker.selectItem(at: 0) - clearLocation(nil) - } - } - - func updateUI() { - guard let watchView = WatchFace.currentInstance?._view else { return } - let watchLayout = WatchLayout.shared - globalMonthPicker.selectItem(at: ChineseCalendar.globalMonth ? 0 : 1) - populateTimezonePicker(timezone: watchView.timezone) - if let time = watchView.displayTime { - datetimePicker.dateValue = time.convertToTimeZone(initTimeZone: Calendar.current.timeZone, timeZone: panelTimezone) - datetimePicker.isEnabled = true - currentTimeToggle.state = .off - } else { - datetimePicker.dateValue = Date().convertToTimeZone(initTimeZone: Calendar.current.timeZone, timeZone: panelTimezone) - datetimePicker.isEnabled = false - currentTimeToggle.state = .on - } - updateLocationUI() - firstRingGradientPicker.gradient = watchLayout.firstRing - secondRingGradientPicker.gradient = watchLayout.secondRing - thirdRingGradientPicker.gradient = watchLayout.thirdRing - toggle(button: firstRingIsLoop, with: watchLayout.firstRing.isLoop) - toggle(button: secondRingIsLoop, with: watchLayout.secondRing.isLoop) - toggle(button: thirdRingIsLoop, with: watchLayout.thirdRing.isLoop) - shadeAlphaValuePicker.doubleValue = Double(watchLayout.shadeAlpha) - shadeAlphaValueLabel.stringValue = String(format: "%1.2f", watchLayout.shadeAlpha) - innerTextGradientPicker.gradient = watchLayout.centerFontColor - innerColorPicker.color = NSColor(cgColor: watchLayout.innerColor)! - majorTickColorPicker.color = NSColor(cgColor: watchLayout.majorTickColor)! - minorTickColorPicker.color = NSColor(cgColor: watchLayout.minorTickColor)! - minorTickAlphaValuePicker.doubleValue = Double(watchLayout.minorTickAlpha) - minorTickAlphaValueLabel.stringValue = String(format: "%1.2f", watchLayout.minorTickAlpha) - majorTickAlphaValuePicker.doubleValue = Double(watchLayout.majorTickAlpha) - majorTickAlphaValueLabel.stringValue = String(format: "%1.2f", watchLayout.majorTickAlpha) - textColorPicker.color = NSColor(cgColor: watchLayout.fontColor)! - oddStermTickColorPicker.color = NSColor(cgColor: watchLayout.oddSolarTermTickColor)! - evenStermTickColorPicker.color = NSColor(cgColor: watchLayout.evenSolarTermTickColor)! - innerColorPickerDark.color = NSColor(cgColor: watchLayout.innerColorDark)! - majorTickColorPickerDark.color = NSColor(cgColor: watchLayout.majorTickColorDark)! - minorTickColorPickerDark.color = NSColor(cgColor: watchLayout.minorTickColorDark)! - textColorPickerDark.color = NSColor(cgColor: watchLayout.fontColorDark)! - oddStermTickColorPickerDark.color = NSColor(cgColor: watchLayout.oddSolarTermTickColorDark)! - evenStermTickColorPickerDark.color = NSColor(cgColor: watchLayout.evenSolarTermTickColorDark)! - mercuryIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[0])! - venusIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[1])! - marsIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[2])! - jupyterIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[3])! - saturnIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[4])! - moonIndicatorColorPicker.color = NSColor(cgColor: watchLayout.planetIndicator[5])! - eclipseIndicatorColorPicker.color = NSColor(cgColor: watchLayout.eclipseIndicator)! - fullmoonIndicatorColorPicker.color = NSColor(cgColor: watchLayout.fullmoonIndicator)! - oddStermIndicatorColorPicker.color = NSColor(cgColor: watchLayout.oddStermIndicator)! - evenStermIndicatorColorPicker.color = NSColor(cgColor: watchLayout.evenStermIndicator)! - midnightIndicatorColorPicker.color = NSColor(cgColor: watchLayout.sunPositionIndicator[0])! - sunriseIndicatorColorPicker.color = NSColor(cgColor: watchLayout.sunPositionIndicator[1])! - noonIndicatorColorPicker.color = NSColor(cgColor: watchLayout.sunPositionIndicator[2])! - sunsetIndicatorColorPicker.color = NSColor(cgColor: watchLayout.sunPositionIndicator[3])! - moonriseIndicatorColorPicker.color = NSColor(cgColor: watchLayout.moonPositionIndicator[0])! - moonmeridianIndicatorColorPicker.color = NSColor(cgColor: watchLayout.moonPositionIndicator[1])! - moonsetIndicatorColorPicker.color = NSColor(cgColor: watchLayout.moonPositionIndicator[2])! - populateFontFamilies(textFontFamilyPicker) - textFontFamilyPicker.selectItem(withTitle: watchLayout.textFont.familyName!) - populateFontMember(textFontTraitPicker, inFamily: textFontFamilyPicker) - if let traits = watchLayout.textFont.fontName.split(separator: "-").last { - textFontTraitPicker.selectItem(withTitle: String(traits)) - if textFontTraitPicker.selectedItem == nil { - textFontTraitPicker.selectItem(at: 0) - } - } - populateFontFamilies(centerTextFontFamilyPicker) - centerTextFontFamilyPicker.selectItem(withTitle: watchLayout.centerFont.familyName!) - populateFontMember(centerTextFontTraitPicker, inFamily: centerTextFontFamilyPicker) - if let traits = watchLayout.centerFont.fontName.split(separator: "-").last { - centerTextFontTraitPicker.selectItem(withTitle: String(traits)) - if centerTextFontTraitPicker.selectedItem == nil { - centerTextFontTraitPicker.selectItem(at: 0) - } - } - widthPicker.stringValue = "\(watchLayout.watchSize.width)" - heightPicker.stringValue = "\(watchLayout.watchSize.height)" - cornerRadiusRatioPicker.stringValue = "\(watchLayout.cornerRadiusRatio)" - centerTextOffsetPicker.stringValue = "\(watchLayout.centerTextOffset)" - centerTextHOffsetPicker.stringValue = "\(watchLayout.centerTextHOffset)" - textHorizontalOffsetPicker.stringValue = "\(watchLayout.horizontalTextOffset)" - textVerticalOffsetPicker.stringValue = "\(watchLayout.verticalTextOffset)" - } - - override func viewWillAppear() { - super.viewWillAppear() - view.window?.delegate = self - guard let watchFace = WatchFace.currentInstance else { return } - if LocationManager.shared.location != nil { - currentLocationToggle.state = .on - } else { - currentLocationToggle.state = .off - } - currentLocationToggled(currentLocationToggle!) - if let window = view.window { - let screen = watchFace.getCurrentScreen() - if watchFace.frame.maxX + 10 + window.frame.width < screen.maxX { - window.setFrameOrigin(NSMakePoint(watchFace.frame.maxX + 10, watchFace.frame.midY - window.frame.height / 2)) - } else { - window.setFrameOrigin(NSMakePoint(max(watchFace.frame.minX - 10 - window.frame.width, screen.minX), watchFace.frame.midY - window.frame.height / 2)) - } - } - } - - override func viewDidLoad() { - Self.currentInstance = self - super.viewDidLoad() - updateUI() - scrollToTop() - } - - override func viewDidDisappear() { - Self.currentInstance = nil - NSColorPanel.shared.showsAlpha = false - NSColorPanel.shared.setTarget(nil) - NSColorPanel.shared.setAction(nil) - NSColorPanel.shared.close() - super.viewDidDisappear() - } -} - -final class FlippedClipView: NSClipView { - override var isFlipped: Bool { - return true - } -} - -final class ClickableStackView: NSStackView { - func didTapHeading() { - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.25 - context.allowsImplicitAnimation = true - guard let card = self.superview as? NSStackView else { return } - var showOrHide = false - for view in card.arrangedSubviews { - if !(view === self) { - view.isHidden.toggle() - showOrHide = view.isHidden - } - } - if let arrow = self.arrangedSubviews.last as? NSImageView { - if showOrHide { - arrow.image = NSImage(systemSymbolName: "chevron.down", accessibilityDescription: "Expand") - } else { - arrow.image = NSImage(systemSymbolName: "chevron.up", accessibilityDescription: "Expand") - } - } - card.superview?.superview?.superview?.layoutSubtreeIfNeeded() - } - } - - override func mouseUp(with event: NSEvent) { - didTapHeading() - super.mouseUp(with: event) - } -} - -final class CardStackView: NSStackView { - override func viewDidChangeEffectiveAppearance() { - super.viewDidChangeEffectiveAppearance() - effectiveAppearance.performAsCurrentDrawingAppearance { - layer?.backgroundColor = NSColor.textBackgroundColor.cgColor - } - } -} - -final class HelpViewController: NSViewController { - static var currentInstance: HelpViewController? - private let parser = MarkdownParser() - @IBOutlet var stackView: NSStackView! - @IBOutlet var contentView: NSView! - - func boldText(line: String, fontSize: CGFloat) -> NSAttributedString { - let boldRanges = line.boldRanges - let attrStr = NSMutableAttributedString() - if !boldRanges.isEmpty { - var boldRangesIndex = boldRanges.startIndex - var startIndex = line.startIndex - while boldRangesIndex < boldRanges.endIndex { - let boldRange = boldRanges[boldRangesIndex] - let plainText = line[startIndex.. screen.minX { - window.setFrameOrigin(NSMakePoint(watchFace.frame.minX - 10 - window.frame.width, watchFace.frame.midY - window.frame.height / 2)) - } else { - window.setFrameOrigin(NSMakePoint(min(watchFace.frame.maxX + 10, screen.maxX - window.frame.width), watchFace.frame.midY - window.frame.height / 2)) - } - } - } - - override func viewDidDisappear() { - Self.currentInstance = nil - super.viewDidDisappear() - } -} - -final class ThemesListViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { - @IBOutlet var tableView: NSTableView! - - var themes: [DataContainer.SavedTheme] = [] - let currentDeviceName = DataContainer.shared.deviceName - - override func viewDidLoad() { - super.viewDidLoad() - tableView.dataSource = self - tableView.delegate = self - - tableView.target = self - tableView.doubleAction = #selector(tableViewDoubleClick(_:)) - - let menu = NSMenu() - menu.addItem(NSMenuItem(title: NSLocalizedString("刪主題", comment: "Confirm to delete theme title"), action: #selector(tableViewEditItemClicked(_:)), keyEquivalent: "\u{08}")) - tableView.menu = menu - - loadThemes() - } - - override func viewWillAppear() { - super.viewWillAppear() - view.window?.minSize = NSSize(width: 450, height: 300) - view.window?.maxSize = NSSize(width: 480, height: 350) - } - - @IBAction func refreshButtonClicked(_ sender: NSButton) { - refresh() - } - - @IBAction func dismissView(_ sender: NSButton) { - dismiss(nil) - } - - @IBAction func readFile(_ sender: Any) { - let panel = NSOpenPanel() - panel.level = NSWindow.Level.floating - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.allowedContentTypes = [.text, .yaml] - panel.title = NSLocalizedString("Select Layout File", comment: "Open File") - panel.message = NSLocalizedString("Choose a layout file to load from", comment: "Warning") - panel.begin { [self] - result in - if result == .OK, let file = panel.url { - do { - let content = try String(contentsOf: file) - let name = file.lastPathComponent - let nameRange = try NSRegularExpression(pattern: "^([^\\.]+)\\.?.*$").firstMatch(in: name, range: NSMakeRange(0, name.utf16.count))!.range(at: 1) - DataContainer.shared.saveLayout(content, name: self.generateNewName(baseName: (name as NSString).substring(with: nameRange))) - self.refresh() - self.tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - } catch { - let alert = NSAlert() - alert.messageText = NSLocalizedString("Load Failed", comment: "Load Failed") - alert.informativeText = error.localizedDescription - alert.alertStyle = .critical - alert.beginSheetModal(for: view.window!) - } - } - } - } - - @IBAction func writeFile(_ sender: Any) { - let row = tableView.selectedRow - if row >= 0 && row < themes.count { - let theme = themes[row] - let panel = NSSavePanel() - panel.level = NSWindow.Level.floating - panel.title = NSLocalizedString("Select Location", comment: "Save File") - panel.nameFieldStringValue = "\(theme.name).txt" - panel.begin { [self] - result in - if result == .OK, let file = panel.url { - do { - if let layout = DataContainer.shared.readSave(name: theme.name, deviceName: theme.deviceName) { - try layout.data(using: .utf8)?.write(to: file, options: .atomicWrite) - } - } catch { - let alert = NSAlert() - alert.messageText = NSLocalizedString("Save Failed", comment: "Save Failed") - alert.informativeText = error.localizedDescription - alert.alertStyle = .critical - alert.beginSheetModal(for: view.window!) - } - } - } - } else { - let alert = NSAlert() - alert.messageText = NSLocalizedString("選定主題先", comment: "No selection when exporting") - alert.alertStyle = .warning - alert.beginSheetModal(for: view.window!) - } - } - - @IBAction func deleteTheme(_ sender: NSButton) { - tableViewEditItemClicked(sender) - } - - @IBAction func addNew(_ sender: NSButton) { - let timeFormatter = DateFormatter() - timeFormatter.dateStyle = .none - timeFormatter.timeStyle = .short - tableView.beginUpdates() - tableView.insertRows(at: IndexSet(integer: 0), withAnimation: .slideDown) - tableView.endUpdates() - - let themeNameView = tableView.view(atColumn: 0, row: 0, makeIfNecessary: false) as! NSTableCellView - themeNameView.textField?.target = self - themeNameView.textField?.action = #selector(preventDeselect(_:)) - tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - tableView.editColumn(0, row: 0, with: nil, select: true) - let modifiedDateView = tableView.view(atColumn: 2, row: 0, makeIfNecessary: false) as! NSTableCellView - modifiedDateView.textField?.stringValue = timeFormatter.string(from: Date()) - let deviceNameView = tableView.view(atColumn: 1, row: 0, makeIfNecessary: false) as! NSTableCellView - deviceNameView.textField?.stringValue = currentDeviceName - - let fileName = generateNewName(baseName: NSLocalizedString("無名", comment: "new theme default name")) - themeNameView.textField?.stringValue = fileName - Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in - themeNameView.textField?.action = #selector(self.newTheme(_:)) - } - } - - func generateNewName(baseName: String) -> String { - var newFileName = baseName - let currentDeviceThemes = themes.filter { $0.deviceName == self.currentDeviceName }.map { $0.name } - var i = 2 - while currentDeviceThemes.contains(newFileName) { - newFileName = baseName + " \(i)" - i += 1 - } - return newFileName - } - - @objc func preventDeselect(_ sender: NSTextField) { - tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) - tableView.editColumn(0, row: 0, with: nil, select: true) - } - - @objc func refresh() { - loadThemes() - tableView.reloadData() - } - - func loadThemes() { - var loadedThemes = DataContainer.shared.listAll() - loadedThemes.sort { left, right in - if left.deviceName == currentDeviceName && right.deviceName == currentDeviceName { - return left.modifiedDate > right.modifiedDate - } else if left.deviceName == currentDeviceName { - return true - } else if right.deviceName == currentDeviceName { - return false - } else { - if left.deviceName != right.deviceName { - return left.deviceName > right.deviceName - } else { - return left.modifiedDate > right.modifiedDate - } - } - } - themes = loadedThemes - } - - func numberOfRows(in tableView: NSTableView) -> Int { - return themes.count - } - - func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard row < themes.count else { return nil } - let theme = themes[row] - let cell: NSTableCellView - switch tableColumn { - case tableView.tableColumns[0]: - cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "NameCell"), owner: nil) as! NSTableCellView - cell.textField?.stringValue = theme.name - cell.textField?.isEditable = true - cell.textField?.target = self - cell.textField?.action = #selector(renameTheme(_:)) - case tableView.tableColumns[1]: - cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DeviceNameCell"), owner: nil) as! NSTableCellView - cell.textField?.stringValue = theme.deviceName - cell.textField?.isEditable = false - case tableView.tableColumns[2]: - cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DateCell"), owner: nil) as! NSTableCellView - - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .none - let timeFormatter = DateFormatter() - timeFormatter.dateStyle = .none - timeFormatter.timeStyle = .short - - if dateFormatter.string(from: theme.modifiedDate) == dateFormatter.string(from: Date()) { - cell.textField?.stringValue = timeFormatter.string(from: theme.modifiedDate) - } else { - cell.textField?.stringValue = dateFormatter.string(from: theme.modifiedDate) - } - cell.textField?.textColor = .secondaryLabelColor - cell.textField?.isEditable = false - default: - return nil - } - return cell - } - - @objc func renameTheme(_ sender: NSTextField) { - let fileName = sender.stringValue - let row = tableView.selectedRow - guard row >= 0 && row < themes.count else { return } - let theme = themes[row] - - if fileName != "" { - let currentDeviceThemes = themes.filter { $0.deviceName == self.currentDeviceName } - if !(currentDeviceThemes.map { $0.name }.contains(fileName)) { - DataContainer.shared.renameSave(name: theme.name, deviceName: theme.deviceName, newName: fileName) - refresh() - return - } - } - - let alert = NSAlert() - alert.messageText = NSLocalizedString("易名", comment: "rename") - alert.informativeText = NSLocalizedString("不得爲空,不得重名", comment: "no blank, no duplicate name") - alert.addButton(withTitle: NSLocalizedString("作罷", comment: "Ok")) - alert.alertStyle = .warning - alert.beginSheetModal(for: view.window!) - tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) - tableView.editColumn(0, row: row, with: nil, select: true) - } - - @objc func newTheme(_ sender: NSTextField) { - let fileName = sender.stringValue - if fileName != "" { - let currentDeviceThemes = themes.filter { $0.deviceName == self.currentDeviceName } - if !(currentDeviceThemes.map { $0.name }.contains(fileName)) { - DataContainer.shared.saveLayout(WatchLayout.shared.encode(), name: fileName) - refresh() - return - } - } - - let alert = NSAlert() - alert.messageText = NSLocalizedString("取名", comment: "set a name") - alert.informativeText = NSLocalizedString("不得爲空,不得重名", comment: "no blank, no duplicate name") - alert.addButton(withTitle: NSLocalizedString("作罷", comment: "Ok")) - alert.alertStyle = .warning - alert.beginSheetModal(for: view.window!) - } - - @objc func tableViewDoubleClick(_ sender: Any) { - let row = tableView.selectedRow - let theme = themes[row] - let alert = NSAlert() - alert.messageText = NSLocalizedString("換主題", comment: "Confirm to select theme title") - alert.informativeText = NSLocalizedString("換爲:", comment: "Confirm to select theme message") + theme.name - alert.addButton(withTitle: NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings")) - alert.addButton(withTitle: NSLocalizedString("容吾三思", comment: "Cancel Resetting Settings")) - alert.alertStyle = .warning - alert.beginSheetModal(for: view.window!) { response in - if response == .alertFirstButtonReturn { - DataContainer.shared.loadSave(name: theme.name, deviceName: theme.deviceName) - WatchFace.currentInstance?.invalidateShadow() - WatchFace.currentInstance?.updateSize() - WatchFace.currentInstance?._view.drawView(forceRefresh: true) - if let parentView = ConfigurationViewController.currentInstance { - parentView.updateUI() - } - } - } - } - - @objc func tableViewEditItemClicked(_ sender: Any?) { - let row = tableView.selectedRow - guard row >= 0 && row < themes.count else { return } - let theme = themes[row] - let alert = NSAlert() - alert.messageText = NSLocalizedString("刪主題", comment: "Confirm to delete theme title") - alert.informativeText = NSLocalizedString("刪:", comment: "Confirm to delete theme message") + theme.name - alert.addButton(withTitle: NSLocalizedString("吾意已決", comment: "Confirm Resetting Settings")) - alert.addButton(withTitle: NSLocalizedString("容吾三思", comment: "Cancel Resetting Settings")) - alert.alertStyle = .warning - alert.beginSheetModal(for: view.window!) { [self] response in - if response == .alertFirstButtonReturn { - DataContainer.shared.deleteSave(name: theme.name, deviceName: theme.deviceName) - refresh() - } - } - } -} - -class SettingBox: NSView { - required init?(coder: NSCoder) { - super.init(coder: coder) - self.wantsLayer = true - self.layer?.cornerRadius = 10 - self.layer?.cornerCurve = .continuous - self.layer?.backgroundColor = NSColor.textBackgroundColor.cgColor - } - - override func viewDidChangeEffectiveAppearance() { - super.viewDidChangeEffectiveAppearance() - effectiveAppearance.performAsCurrentDrawingAppearance { - layer?.backgroundColor = NSColor.textBackgroundColor.cgColor - } - } -} diff --git a/macOS/Views/Setting.swift b/macOS/Views/Setting.swift new file mode 100644 index 0000000..f388e56 --- /dev/null +++ b/macOS/Views/Setting.swift @@ -0,0 +1,145 @@ +// +// SwiftUIView.swift +// Chinese Time +// +// Created by Leo Liu on 6/30/23. +// + +import SwiftUI +import WidgetKit + +struct Setting: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.watchSetting) var watchSetting + @State private var selection: WatchSetting.Selection? = .none + @State private var columnVisibility = NavigationSplitViewVisibility.automatic + @Environment(\.modelContext) private var modelContext + + var body: some View { + NavigationSplitView(columnVisibility: $columnVisibility) { + List(selection: $selection) { + Section(header: Text("數據", comment: "Data Source")) { + 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 + buildView(selection: selection) + } + } + Section(header: Text("其它", comment: "Miscellaneous")) { + ForEach([WatchSetting.Selection.themes, WatchSetting.Selection.documentation], id: \.self) { selection in + buildView(selection: selection) + } + } + } + } detail: { + switch selection { + case .datetime: + Datetime() + case .location: + Location() + case .ringColor: + RingSetting() + case .markColor: + ColorSetting() + case .layout: + LayoutSetting() + case .themes: + ThemesList() + case .documentation: + Documentation() + case .none: + EmptyView() + } + } + .animation(.easeInOut, value: columnVisibility) + .toolbar { + ToolbarItem(placement: .navigation) { + Button { + if columnVisibility != .detailOnly { + columnVisibility = .detailOnly + } else { + columnVisibility = .all + } + } label: { + Image(systemName: "sidebar.leading") + } + } + } + .navigationSplitViewStyle(.balanced) + .onChange(of: selection) { _, _ in + cleanColorPanel() + } + .onAppear { + selection = watchSetting.previousSelection ?? .datetime + } + .onDisappear { + watchSetting.previousSelection = selection + selection = .none + watchLayout.saveDefault(context: modelContext) + WidgetCenter.shared.reloadAllTimelines() + AppDelegate.instance?.lastReloaded = .now + cleanColorPanel() + } + } + + func buildView(selection: WatchSetting.Selection) -> some View { + let sel = switch selection { + case .datetime: + Label { + Text("日時", comment: "Display time settings") + } icon: { + Image(systemName: "clock") + } + case .location: + Label { + Text("經緯度", comment: "Geo Location section") + } icon: { + Image(systemName: "location") + } + case .ringColor: + Label { + Text("輪色", comment: "Rings Color Setting") + } icon: { + Image(systemName: "pencil.and.outline") + } + case .markColor: + Label { + Text("色塊", comment: "Mark Color settings") + } icon: { + Image(systemName: "wand.and.stars") + } + case .layout: + Label { + Text("佈局", comment: "Layout settings section") + } icon: { + Image(systemName: "square.resize") + } + case .themes: + Label { + Text("主題庫", comment: "manage saved themes") + } icon: { + Image(systemName: "archivebox") + } + case .documentation: + Label { + Text("註釋", comment: "Documentation View") + } icon: { + Image(systemName: "doc.questionmark") + } + } + return sel + } + + @MainActor func cleanColorPanel() { + NSColorPanel.shared.setTarget(nil) + NSColorPanel.shared.setAction(nil) + NSColorPanel.shared.close() + } +} + +#Preview("Settings") { + Setting() +} diff --git a/macOS/Views/WatchFace.swift b/macOS/Views/WatchFace.swift new file mode 100644 index 0000000..a7df342 --- /dev/null +++ b/macOS/Views/WatchFace.swift @@ -0,0 +1,76 @@ +// +// WatchFace.swift +// Chinese Time mac +// +// Created by Leo Liu on 7/1/23. +// + +import SwiftUI + +@MainActor +struct WatchFace: View { + @Environment(\.watchLayout) var watchLayout + @Environment(\.chineseCalendar) var chineseCalendar + @State var entityPresenting = EntitySelection() + @State var tapPos: CGPoint? = nil + @State var hoverBounds: CGRect = .zero + @GestureState var longPressed = false + @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 + } + } + + var body: some View { + GeometryReader { proxy in + let size: CGSize = watchLayout.watchSize + let centerOffset = if size.height >= size.width { + watchLayout.centerTextOffset + } else { + watchLayout.centerTextHOffset + } + + 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) + .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)) + + Hover(entityPresenting: entityPresenting, bounds: $hoverBounds, tapPos: $tapPos) + } + .animation(.easeInOut(duration: 0.2), value: entityPresenting.activeNote) + } + .frame(width: watchLayout.watchSize.width, height: watchLayout.watchSize.height) + } +} + +#Preview("WatchFace") { + WatchFace() + .modelContainer(ThemeData.container) +} diff --git a/macOS/Views/Welcome.swift b/macOS/Views/Welcome.swift new file mode 100644 index 0000000..f49624e --- /dev/null +++ b/macOS/Views/Welcome.swift @@ -0,0 +1,58 @@ +// +// Welcome.swift +// Chinese Time mac +// +// Created by Leo Liu on 8/4/23. +// + +import SwiftUI + +struct Welcome: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(spacing: 20) { + Image(.image) + .resizable() + .frame(width: 120, height: 120) + Text("華曆", comment: "Chinese Time") + .font(.largeTitle.bold()) + HStack { + Image(systemName: "menubar.rectangle") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("常駐狀態欄", comment: "Welcome, ring design - title") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 5) + Text("華曆顯示於右上角狀態欄,點它展開", comment: "Welcome, display at status bar") + } + } + .padding(.top, 5) + HStack { + Image(systemName: "pencil.and.outline") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("設置與詳述", comment: "Welcome, long press - title") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 5) + Text("展開後點齒輪進設置,按你心意裝點最美華曆,其內亦有華曆詳述", comment: "Welcome, setting and documentation") + } + } + } + .padding(20) + } +} + + +#Preview("Welcome") { + Welcome() + .frame(minWidth: 300, idealWidth: 350, maxWidth: 400, minHeight: 400, idealHeight: 600, maxHeight: 700, alignment: .center) +} diff --git a/macOS/WatchFace.swift b/macOS/WatchFace.swift deleted file mode 100644 index 93ce41a..0000000 --- a/macOS/WatchFace.swift +++ /dev/null @@ -1,536 +0,0 @@ -// -// watchFace.swift -// ChineseTime -// -// Created by LEO Yoon-Tsaw on 9/19/21. -// - -import AppKit - -final class WatchFaceView: NSView { - static let frameOffset: CGFloat = 5 - - let watchLayout: WatchLayout = .shared - var displayTime: Date? = nil - var timezone: TimeZone = Calendar.current.timeZone - var shape: CAShapeLayer = .init() - var phase: StartingPhase = .init(zeroRing: 0, firstRing: 0, secondRing: 0, thirdRing: 0, fourthRing: 0) - var entityNotes: [EntityNote] = [] - var tooltipView: NoteView? - - var cornerSize: CGFloat = 0.3 - private var chineseCalendar = ChineseCalendar(time: Date(), timezone: TimeZone.current, location: nil) - - var graphicArtifects = GraphicArtifects() - private var keyStates = KeyStates() - - var location: CGPoint? { - LocationManager.shared.location ?? watchLayout.location - } - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.wantsLayer = true - layer?.masksToBounds = true - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // Need flipped coordinate system, as required by textStorage - override var isFlipped: Bool { - true - } - - var isDark: Bool { - self.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua - } - - override func viewDidChangeEffectiveAppearance() { - layer?.sublayers = nil - graphicArtifects = GraphicArtifects() - needsDisplay = true - } - - func update() { - let time = displayTime ?? Date() - chineseCalendar.update(time: time, timezone: timezone, location: location) - } - - func drawView(forceRefresh: Bool) { - layer?.sublayers = nil - update() - if forceRefresh { - graphicArtifects = GraphicArtifects() - } - needsDisplay = true - } - - func updateStatusBar(title: String) { - if String(title.reversed()) != Chinese_Time.statusItem?.button?.title { - Chinese_Time.updateStatusTitle(title: title) - } - } - - override func draw(_ rawRect: NSRect) { - let dirtyRect = rawRect.insetBy(dx: Self.frameOffset, dy: Self.frameOffset) - if graphicArtifects.outerBound == nil { - let shortEdge = min(dirtyRect.width, dirtyRect.height) - shape.path = RoundedRect(rect: dirtyRect, nodePos: shortEdge * 0.08, ankorPos: shortEdge * 0.08 * 0.2).path - } - entityNotes = (layer!.update(dirtyRect: dirtyRect, isDark: isDark, watchLayout: watchLayout, chineseCalendar: chineseCalendar, graphicArtifects: graphicArtifects, keyStates: keyStates, phase: phase)) - updateStatusBar(title: "\(chineseCalendar.dateString) \(chineseCalendar.timeString)") - } - - override func rightMouseUp(with event: NSEvent) { - let shortEdge = min(bounds.width, bounds.height) - let point = convert(event.locationInWindow, from: nil) - var entities = [EntityNote]() - for entity in entityNotes { - let diff = point - entity.position - let dist = sqrt(diff.x * diff.x + diff.y * diff.y) - if dist.isFinite && dist < GraphicArtifects.markRadius * 2 * shortEdge { - entities.append(entity) - } - } - if let contentView = window?.contentView, entities.count > 0 { - let center = convert(point, to: contentView) - let boundingRect = contentView.bounds.insetBy(dx: Self.frameOffset, dy: Self.frameOffset) - let tooltip = NoteView(center: center, bounds: boundingRect, entities: entities) - tooltipView?.removeFromSuperview() - tooltipView = tooltip - if let tooltip = tooltip { - contentView.addSubview(tooltip) - } - } - super.rightMouseUp(with: event) - } -} - -final class NoteView: NSView { - private var visualEffectView: NSVisualEffectView! - private var entities: [EntityNote] = [] - - init?(center: NSPoint, bounds: NSRect, entities: [EntityNote]) { - var entities = entities - let width: CGFloat - let height: CGFloat - if Locale.isChinese { - width = CGFloat(entities.count) * (NSFont.systemFontSize + 6) + 8 - height = CGFloat(entities.map { $0.name.count }.reduce(0) { max($0, $1) }) * (NSFont.systemFontSize + 2) + 32 - } else { - for i in 0 ..< entities.count { - entities[i].name = Locale.translation[entities[i].name] ?? entities[i].name - } - width = CGFloat(entities.map { NSAttributedString(string: $0.name, attributes: [.font: NSFont.systemFont(ofSize: NSFont.systemFontSize)]).boundingRect(with: .zero, context: .none).width }.reduce(0) { max($0, $1) }) + 32 - height = CGFloat(entities.count) * (NSFont.systemFontSize + 6) + 6 - } - - var frame = CGRect(x: center.x - width / 2, y: center.y - height / 2, width: width, height: height) - - if frame.maxX > bounds.maxX { - frame.origin.x -= frame.maxX - bounds.maxX - } - if frame.maxY > bounds.maxY { - frame.origin.y -= frame.maxY - bounds.maxY - } - if frame.minX >= bounds.minX && frame.minY >= bounds.minY { - super.init(frame: frame) - self.entities = entities - self.shadow = { () -> NSShadow in - let shadow = NSShadow() - shadow.shadowBlurRadius = 5 - shadow.shadowOffset = CGSize(width: 3, height: -3) - shadow.shadowColor = .black.withAlphaComponent(0.2) - return shadow - }() - setupView() - } else { - return nil - } - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - - override func viewWillDraw() { - super.viewWillDraw() - alphaValue = 0 - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - self.animator().alphaValue = 1 - } - Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.2 - self.animator().alphaValue = 0 - }) { - self.removeFromSuperview() - } - } - } - - private func setupView() { - visualEffectView = NSVisualEffectView(frame: bounds) - visualEffectView.autoresizingMask = [.width, .height] - visualEffectView.blendingMode = .withinWindow - visualEffectView.state = .active - addSubview(visualEffectView) - - visualEffectView.wantsLayer = true - let mask = CAShapeLayer() - mask.path = RoundedRect(rect: visualEffectView.frame, nodePos: 10, ankorPos: 2).path - visualEffectView.layer?.mask = mask - - var lastView: NSView? = nil - let isChinese = Locale.isChinese - for entity in entities.reversed() { - let entityView = createEntityView(for: entity, isChinese: isChinese) - visualEffectView.addSubview(entityView) - entityView.translatesAutoresizingMaskIntoConstraints = false - if isChinese { - if let lastView = lastView { - entityView.trailingAnchor.constraint(equalTo: lastView.leadingAnchor, constant: -4).isActive = true - } else { - entityView.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -6).isActive = true - } - entityView.topAnchor.constraint(equalTo: visualEffectView.topAnchor, constant: 6).isActive = true - entityView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -6).isActive = true - entityView.widthAnchor.constraint(equalToConstant: NSFont.systemFontSize + 2).isActive = true - } else { - if let lastView = lastView { - entityView.bottomAnchor.constraint(equalTo: lastView.topAnchor, constant: 0).isActive = true - } else { - entityView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -4).isActive = true - } - entityView.leadingAnchor.constraint(equalTo: visualEffectView.leadingAnchor, constant: 6).isActive = true - entityView.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -6).isActive = true - entityView.heightAnchor.constraint(equalToConstant: NSFont.systemFontSize + 6).isActive = true - } - lastView = entityView - } - } - - private func createEntityView(for entity: EntityNote, isChinese: Bool) -> NSView { - let view = NSView() - - let colorMark = NSView() - colorMark.wantsLayer = true - colorMark.layer?.backgroundColor = entity.color - let mask = CAShapeLayer() - mask.path = RoundedRect(rect: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: 11, height: 11)), nodePos: 6 * 0.7, ankorPos: 6 * 0.3).path - colorMark.layer?.mask = mask - view.addSubview(colorMark) - - let label = NSTextField() - label.drawsBackground = false - label.isBezeled = false - label.font = NSFont.systemFont(ofSize: NSFont.systemFontSize) - view.addSubview(label) - - colorMark.translatesAutoresizingMaskIntoConstraints = false - label.translatesAutoresizingMaskIntoConstraints = false - - if isChinese { - label.stringValue = entity.name.map { String($0) }.joined(separator: "\n") - label.alignment = .right - - NSLayoutConstraint.activate([ - colorMark.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -1), - colorMark.topAnchor.constraint(equalTo: view.topAnchor, constant: 2), - colorMark.widthAnchor.constraint(equalToConstant: 12), - colorMark.heightAnchor.constraint(equalToConstant: 12), - - label.topAnchor.constraint(equalTo: colorMark.bottomAnchor, constant: 4), - label.trailingAnchor.constraint(equalTo: view.trailingAnchor), - label.widthAnchor.constraint(equalTo: view.widthAnchor) - ]) - - } else { - label.stringValue = entity.name - label.alignment = .left - label.maximumNumberOfLines = 1 - - NSLayoutConstraint.activate([ - colorMark.topAnchor.constraint(equalTo: view.topAnchor, constant: 4.5), - colorMark.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2), - colorMark.widthAnchor.constraint(equalToConstant: 12), - colorMark.heightAnchor.constraint(equalToConstant: 12), - - label.leadingAnchor.constraint(equalTo: colorMark.trailingAnchor, constant: 4), - label.trailingAnchor.constraint(equalTo: view.trailingAnchor), - label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 2), - label.heightAnchor.constraint(equalTo: view.heightAnchor) - ]) - } - - return view - } -} - -final class OptionView: NSView { - let background: NSVisualEffectView - let button: NSButton - override var frame: NSRect { - didSet { - self.background.frame = self.bounds - self.button.frame = self.bounds - (background.layer?.mask as? CAShapeLayer)?.path = RoundedRect(rect: background.bounds, nodePos: background.bounds.height / 2, ankorPos: background.bounds.height / 2 * 0.2).path - } - } - - override init(frame frameRect: NSRect) { - let background = NSVisualEffectView(frame: frameRect) - background.blendingMode = .behindWindow - background.material = .popover - background.state = .active - background.wantsLayer = true - let optionMask = CAShapeLayer() - optionMask.path = RoundedRect(rect: background.bounds, nodePos: background.bounds.height / 2, ankorPos: background.bounds.height / 2 * 0.2).path - background.layer?.mask = optionMask - - let button = NSButton(frame: frameRect) - button.alignment = .center - button.isBordered = false - - self.background = background - self.button = button - super.init(frame: frameRect) - addSubview(self.background) - addSubview(self.button) - } - - required init?(coder: NSCoder) { - self.background = NSVisualEffectView() - self.button = NSButton() - super.init(coder: coder) - } - - var title: String { - get { - button.title - } set { - button.title = newValue - } - } - - var image: NSImage? { - get { - button.image - } set { - button.image = newValue - } - } - - var action: Selector? { - get { - button.action - } set { - button.action = newValue - } - } - - var target: AnyObject? { - get { - button.target - } set { - button.target = newValue - } - } -} - -final class WatchFace: NSPanel { - let _view: WatchFaceView - let _backView: NSVisualEffectView - let _settingButton: OptionView - let _helpButton: OptionView - let _closingButton: OptionView - private var _timer: Timer? - static var currentInstance: WatchFace? = nil - private static let updateInterval: CGFloat = 14.4 - var buttonSize: NSSize { - let ratio = 80 * 2.3 / (WatchLayout.shared.watchSize.width / 2) - return NSMakeSize(60 / ratio, 30 / ratio) - } - - init(position: NSRect) { - self._view = WatchFaceView(frame: position) - let blurView = NSVisualEffectView() - blurView.blendingMode = .behindWindow - blurView.material = .popover - blurView.state = .active - blurView.wantsLayer = true - self._backView = blurView - self._settingButton = { - let view = OptionView(frame: NSZeroRect) - view.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Setting") - view.button.contentTintColor = .systemGray - return view - }() - self._helpButton = { - let view = OptionView(frame: NSZeroRect) - view.image = NSImage(systemSymbolName: "info.bubble", accessibilityDescription: "Help") - view.button.contentTintColor = .systemGray - return view - }() - self._closingButton = { - let view = OptionView(frame: NSZeroRect) - view.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Quit") - view.button.contentTintColor = .systemRed - return view - }() - super.init(contentRect: position, styleMask: .borderless, backing: .buffered, defer: true) - _settingButton.target = self - _settingButton.action = #selector(openSetting(_:)) - _helpButton.target = self - _helpButton.action = #selector(openHelp(_:)) - _closingButton.target = self - _closingButton.action = #selector(closeApp(_:)) - self.alphaValue = 1 - self.level = NSWindow.Level.floating - self.hasShadow = true - self.isOpaque = false - self.backgroundColor = .clear - let contentView = NSView() - self.contentView = contentView - contentView.addSubview(_backView) - contentView.addSubview(_view) - contentView.addSubview(_settingButton) - contentView.addSubview(_helpButton) - contentView.addSubview(_closingButton) - self.isMovableByWindowBackground = false - } - - @objc func openSetting(_ sender: NSButton) { - if let congigurationView = ConfigurationViewController.currentInstance { - congigurationView.view.window?.close() - } else { - let storyboard = NSStoryboard(name: "Main", bundle: nil) - let windowController = storyboard.instantiateController(withIdentifier: "WindowController") as! NSWindowController - if let window = windowController.window { - let viewController = window.contentViewController as! ConfigurationViewController - ConfigurationViewController.currentInstance = viewController - windowController.showWindow(nil) - } - } - } - - @objc func openHelp(_ sender: NSButton) { - if let helperView = HelpViewController.currentInstance { - helperView.view.window?.close() - } else { - let storyboard = NSStoryboard(name: "Main", bundle: nil) - let windowController = storyboard.instantiateController(withIdentifier: "HelpWindow") as! NSWindowController - if let window = windowController.window { - let viewController = window.contentViewController as! HelpViewController - HelpViewController.currentInstance = viewController - windowController.showWindow(nil) - } - } - } - - @objc func closeApp(_ sender: NSButton) { - NSApp.terminate(sender) - } - - override var isVisible: Bool { - get { - contentView != nil && !contentView!.isHidden - } set { - contentView?.isHidden = !newValue - } - } - - func setCenter() { - let windowRect = getCurrentScreen() - setFrame(NSMakeRect( - windowRect.midX - WatchLayout.shared.watchSize.width / 2, - windowRect.midY - WatchLayout.shared.watchSize.height / 2 - buttonSize.height * 0.85, - WatchLayout.shared.watchSize.width, - WatchLayout.shared.watchSize.height + buttonSize.height * 1.7), display: true) - } - - func moveTopCenter(to: CGPoint) { - let windowRect = getCurrentScreen() - var frame = NSMakeRect( - to.x - WatchLayout.shared.watchSize.width / 2, - to.y - WatchLayout.shared.watchSize.height - buttonSize.height * 1.7, - WatchLayout.shared.watchSize.width, - WatchLayout.shared.watchSize.height + buttonSize.height * 1.7) - if NSMaxX(frame) >= NSMaxX(windowRect) { - frame.origin.x = NSMaxX(windowRect) - frame.width - } else if NSMinX(frame) <= NSMinX(windowRect) { - frame.origin.x = NSMinX(windowRect) - } - setFrame(frame, display: true) - } - - func getCurrentScreen() -> NSRect { - var screenRect = NSScreen.main!.frame - let screens = NSScreen.screens - for i in 0 ..< screens.count { - let rect = screens[i].frame - if let statusBarFrame = Chinese_Time.statusItem?.button?.window?.frame, NSPointInRect(NSMakePoint(statusBarFrame.midX, statusBarFrame.midY), rect) { - screenRect = rect - break - } - } - return screenRect - } - - func updateSize() { - let watchDimension = WatchLayout.shared.watchSize - let buttonSize = buttonSize - if frame.isEmpty { - setCenter() - } else { - setFrame(NSMakeRect( - frame.midX - watchDimension.width / 2, - frame.midY - watchDimension.height / 2 - buttonSize.height * 0.85, - watchDimension.width, watchDimension.height + buttonSize.height * 1.7), display: true) - } - - var bounds = _view.superview!.bounds - bounds.origin.y += buttonSize.height * 1.7 - bounds.size.height -= buttonSize.height * 1.7 - _view.frame = bounds - _backView.frame = bounds - _settingButton.frame = NSMakeRect(bounds.width / 2 - buttonSize.width * 2, buttonSize.height / 2, buttonSize.width, buttonSize.height) - _settingButton.button.font = _settingButton.button.font?.withSize(buttonSize.height / 2) - _helpButton.frame = NSMakeRect(bounds.width / 2 - buttonSize.width * 0.5, buttonSize.height / 2, buttonSize.width, buttonSize.height) - _helpButton.button.font = _helpButton.button.font?.withSize(buttonSize.height / 2) - _closingButton.frame = NSMakeRect(bounds.width / 2 + buttonSize.width, buttonSize.height / 2, buttonSize.width, buttonSize.height) - _closingButton.button.font = _closingButton.button.font?.withSize(buttonSize.height / 2) - _view.cornerSize = WatchLayout.shared.cornerRadiusRatio * min(watchDimension.width, watchDimension.height) - } - - func show() { - updateSize() - _view.drawView(forceRefresh: true) - invalidateShadow() - _view.drawView(forceRefresh: false) - _backView.layer?.mask = _view.shape - orderFront(nil) - isVisible = true - _timer = Timer.scheduledTimer(withTimeInterval: Self.updateInterval, repeats: true) { _ in - self.invalidateShadow() - self._view.drawView(forceRefresh: false) - } - Self.currentInstance = self - } - - func hide() { - isVisible = false - } - - override func close() { - _timer?.invalidate() - _timer = nil - Self.currentInstance = nil - super.close() - } -} diff --git a/macOS/en.lproj/InfoPlist.strings b/macOS/en.lproj/InfoPlist.strings deleted file mode 100644 index f07b460..0000000 --- a/macOS/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,15 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "Chinese Time"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Open source under GPL v3"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "Provide location to calculate local sunrise/set, moon rise/set time. Do not affect other functions if rejected."; - diff --git a/macOS/en.lproj/Localizable.strings b/macOS/en.lproj/Localizable.strings deleted file mode 100644 index efff8ff..0000000 --- a/macOS/en.lproj/Localizable.strings +++ /dev/null @@ -1,84 +0,0 @@ -/* Cancel button title */ -"Cancel" = "Cancel"; - -/* Warning */ -"Choose a layout file to load from" = "Choose a layout file to load from"; - -/* Quit without saves error question message */ -"Could not save changes while quitting. Quit anyway?" = "Could not save changes while quitting. Quit anyway?"; - -/* Default save file name */ -"Default" = "Default"; - -/* Load Failed */ -"Load Failed" = "Load Failed"; - -/* Quit anyway button title */ -"Quit anyway" = "Quit anyway"; - -/* Quit without saves error question info */ -"Quitting now will lose any changes you have made since the last successful save" = "Quitting now will lose any changes you have made since the last successful save"; - -/* Save Failed */ -"Save Failed" = "Save Failed"; - -/* Save File */ -"Select File" = "Select File"; - -/* Open File */ -"Select Layout File" = "Select Layout File"; - -/* Save File */ -"Select Location" = "Select Location"; - -/* Warning */ -"Warning: The current layout will be discarded!" = "Warning: The current layout will be discarded!"; - -/* no blank, no duplicate name */ -"不得爲空,不得重名" = "No empty name, no duplicated name"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# Why create this Chinese Time?\nIs 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, 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 亥."; - -/* Ok */ -"作罷" = "OK"; - -/* Confirm to delete theme message */ -"刪:" = "Delete: "; - -/* Confirm to delete theme title */ -"刪主題" = "Delete Theme"; - -/* set a name */ -"取名" = "Set Name"; - -/* Confirm Resetting Settings */ -"吾意已決" = "Proceed"; - -/* Cancel Resetting Settings */ -"容吾三思" = "Cancel"; - -/* Alert: Location service disabled */ -"怪哉" = "Sorry"; - -/* Confirm to select theme title */ -"換主題" = "Switch Theme"; - -/* Confirm to select theme message */ -"換爲:" = "Switch to: "; - -/* rename */ -"易名" = "Rename"; - -/* new theme default name */ -"無名" = "Unnamed"; - -/* Unknown saved file */ -"神祕檔" = "Mysterious theme"; - -/* Please enable location service to obtain your longitude and latitude */ -"蓋因定位未開啓" = "Location service is disabled"; - -/* No selection when exporting */ -"選定主題先" = "Make a selection first"; - diff --git a/macOS/en.lproj/Main.strings b/macOS/en.lproj/Main.strings deleted file mode 100644 index 6b66567..0000000 --- a/macOS/en.lproj/Main.strings +++ /dev/null @@ -1,300 +0,0 @@ -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "0Bo-Tc-KcV"; */ -"0Bo-Tc-KcV.title" = "Item 1"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "0WZ-lt-4qc"; */ -"0WZ-lt-4qc.title" = "Loop"; - -/* Class = "NSMenuItem"; title = "Chinese Time"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "Chinese Time"; - -/* Class = "NSButtonCell"; title = "今時"; ObjectID = "3mu-Kk-md7"; */ -"3mu-Kk-md7.title" = "Now"; - -/* Class = "NSWindow"; title = "注釋"; ObjectID = "4qE-Yy-bnn"; */ -"4qE-Yy-bnn.title" = "Documentation"; - -/* Class = "NSMenuItem"; title = "Quit Chinese Time"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "Quit Chinese Time"; - -/* Class = "NSButtonCell"; title = "主題庫"; ObjectID = "5zZ-P4-UFo"; */ -"5zZ-P4-UFo.title" = "Saved Themes"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "6g9-iA-NU6"; */ -"6g9-iA-NU6.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʺ"; ObjectID = "6GB-kh-C3O"; */ -"6GB-kh-C3O.placeholderString" = "0ʺ"; - -/* Class = "NSTextFieldCell"; title = "小字體"; ObjectID = "6nN-DT-d84"; */ -"6nN-DT-d84.title" = "Text Font"; - -/* Class = "NSSegmentedCell"; 6Rv-hF-6sh.ibShadowedLabels[0] = "E"; ObjectID = "6Rv-hF-6sh"; */ -"6Rv-hF-6sh.ibShadowedLabels[0]" = "E"; - -/* Class = "NSSegmentedCell"; 6Rv-hF-6sh.ibShadowedLabels[1] = "W"; ObjectID = "6Rv-hF-6sh"; */ -"6Rv-hF-6sh.ibShadowedLabels[1]" = "W"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "7kl-LP-siJ"; */ -"7kl-LP-siJ.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; placeholderString = "0°"; ObjectID = "9wL-YO-Fvc"; */ -"9wL-YO-Fvc.placeholderString" = "0°"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "34d-4a-LV9"; */ -"34d-4a-LV9.title" = "Item 3"; - -/* Class = "NSButtonCell"; title = "畢"; ObjectID = "36y-D0-7sG"; */ -"36y-D0-7sG.title" = "Done"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "88I-Qx-JWn"; */ -"88I-Qx-JWn.title" = "Text Cell"; - -/* Class = "NSTextFieldCell"; title = "大字體"; ObjectID = "327-wT-qfw"; */ -"327-wT-qfw.title" = "Center Font"; - -/* Class = "NSTextFieldCell"; title = "中氣色"; ObjectID = "507-LI-4nD"; */ -"507-LI-4nD.title" = "Even S Term"; - -/* Class = "NSTextFieldCell"; title = "小刻色"; ObjectID = "523-Rm-pQ5"; */ -"523-Rm-pQ5.title" = "Minor Tick"; - -/* Class = "NSTextFieldCell"; title = "日圈色"; ObjectID = "Acb-cx-xKE"; */ -"Acb-cx-xKE.title" = "Day Ring"; - -/* Class = "NSTextFieldCell"; title = "Chinese Time by Leo Liu"; ObjectID = "aMv-K7-khV"; */ -"aMv-K7-khV.title" = "Chinese Time by Leo Liu"; - -/* Class = "NSButtonCell"; title = "畢"; ObjectID = "aOL-ci-PVf"; */ -"aOL-ci-PVf.title" = "Done"; - -/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ -"AYu-sK-qS6.title" = "Main Menu"; - -/* Class = "NSTextFieldCell"; title = "小字平移"; ObjectID = "BGQ-oh-Lvr"; */ -"BGQ-oh-Lvr.title" = "Text H Offset"; - -/* Class = "NSTextFieldCell"; title = "大字縱移"; ObjectID = "BKn-Hv-5S1"; */ -"BKn-Hv-5S1.title" = "Center Text V Offset"; - -/* Class = "NSTextFieldCell"; title = "辰星色"; ObjectID = "C7q-vR-gUk"; */ -"C7q-vR-gUk.title" = "Mercury"; - -/* Class = "NSButtonCell"; title = "應用"; ObjectID = "CJ3-Yd-8ff"; */ -"CJ3-Yd-8ff.title" = "Apply"; - -/* Class = "NSTextFieldCell"; title = "月色"; ObjectID = "cjC-Ct-irg"; */ -"cjC-Ct-irg.title" = "Moon"; - -/* Class = "NSSegmentedCell"; Ckq-JL-XzL.ibShadowedLabels[0] = "N"; ObjectID = "Ckq-JL-XzL"; */ -"Ckq-JL-XzL.ibShadowedLabels[0]" = "N"; - -/* Class = "NSSegmentedCell"; Ckq-JL-XzL.ibShadowedLabels[1] = "S"; ObjectID = "Ckq-JL-XzL"; */ -"Ckq-JL-XzL.ibShadowedLabels[1]" = "S"; - -/* Class = "NSTextFieldCell"; title = "大刻透明"; ObjectID = "D0x-kL-iSv"; */ -"D0x-kL-iSv.title" = "Major Tick Transparency"; - -/* Class = "NSMenuItem"; title = "精確至日"; ObjectID = "D6Z-91-ZaT"; */ -"D6Z-91-ZaT.title" = "Daily Precision"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʹ"; ObjectID = "d72-kj-YSj"; */ -"d72-kj-YSj.placeholderString" = "0ʹ"; - -/* Class = "NSTextFieldCell"; title = "大刻色"; ObjectID = "daY-1b-12h"; */ -"daY-1b-12h.title" = "Major Tick"; - -/* Class = "NSTextFieldCell"; title = "年圈色"; ObjectID = "dff-rx-wxj"; */ -"dff-rx-wxj.title" = "Year Ring"; - -/* Class = "NSTextFieldCell"; title = "夜中色"; ObjectID = "dgh-87-b86"; */ -"dgh-87-b86.title" = "Midnight"; - -/* Class = "NSTextFieldCell"; title = "月出色"; ObjectID = "DNM-Du-Mk0"; */ -"DNM-Du-Mk0.title" = "Moonrise"; - -/* Class = "NSTextFieldCell"; title = "經緯度"; ObjectID = "DRr-dI-0Vw"; */ -"DRr-dI-0Vw.title" = "Geo Location"; - -/* Class = "NSTextFieldCell"; title = "寬"; ObjectID = "Dt7-Kj-jz1"; */ -"Dt7-Kj-jz1.title" = "Width"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "DYi-IC-KBp"; */ -"DYi-IC-KBp.title" = "Loop"; - -/* Class = "NSTextFieldCell"; title = "日入色"; ObjectID = "ECg-3b-kMh"; */ -"ECg-3b-kMh.title" = "Sunset"; - -/* Class = "NSWindow"; title = "設置"; ObjectID = "ECM-Fk-e9r"; */ -"ECM-Fk-e9r.title" = "Settings"; - -/* Class = "NSMenuItem"; title = "標準時"; ObjectID = "F9Y-Uq-Kvt"; */ -"F9Y-Uq-Kvt.title" = "Standard Time"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "FgB-zA-LTz"; */ -"FgB-zA-LTz.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "內核色"; ObjectID = "fjH-PX-sg1"; */ -"fjH-PX-sg1.title" = "Core Back"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "fPn-WR-Q0a"; */ -"fPn-WR-Q0a.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "月入色"; ObjectID = "Gkh-ME-dxX"; */ -"Gkh-ME-dxX.title" = "Moonset"; - -/* Class = "NSTextFieldCell"; title = "置閏法"; ObjectID = "gOr-wQ-sKV"; */ -"gOr-wQ-sKV.title" = "Leap Month"; - -/* Class = "NSMenuItem"; title = "真太陽時"; ObjectID = "hIW-FP-Vjc"; */ -"hIW-FP-Vjc.title" = "Apparent S Time"; - -/* Class = "NSTextFieldCell"; title = "顯示時間"; ObjectID = "HSN-Sc-b1q"; */ -"HSN-Sc-b1q.title" = "Display Time"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "Iz4-69-Iua"; */ -"Iz4-69-Iua.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; title = "小字色"; ObjectID = "j24-Kl-HOr"; */ -"j24-Kl-HOr.title" = "Text Color"; - -/* Class = "NSTextFieldCell"; title = "小字縱移"; ObjectID = "jVV-K8-M9e"; */ -"jVV-K8-M9e.title" = "Text V Offset"; - -/* Class = "NSMenuItem"; title = "精確至時刻"; ObjectID = "jwN-LD-oM5"; */ -"jwN-LD-oM5.title" = "Finest Precision"; - -/* Class = "NSTextFieldCell"; placeholderString = "0°"; ObjectID = "l3A-Gk-ezv"; */ -"l3A-Gk-ezv.placeholderString" = "0°"; - -/* Class = "NSTextFieldCell"; title = "節色"; ObjectID = "l14-gO-pPw"; */ -"l14-gO-pPw.title" = "Odd S Term"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "LjG-ay-Nbv"; */ -"LjG-ay-Nbv.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "節標記色"; ObjectID = "loO-Wq-y3o"; */ -"loO-Wq-y3o.title" = "OST Mark"; - -/* Class = "NSTextFieldCell"; title = "日出色"; ObjectID = "LV5-B3-f4g"; */ -"LV5-B3-f4g.title" = "Sunrise"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "mbO-lM-Hhe"; */ -"mbO-lM-Hhe.title" = "Loop"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "mMa-86-JbP"; */ -"mMa-86-JbP.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "時間"; ObjectID = "mWl-tT-NW1"; */ -"mWl-tT-NW1.title" = "Solar Time"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "MYn-FC-mCk"; */ -"MYn-FC-mCk.title" = "Item 2"; - -/* Class = "NSTextFieldCell"; title = "熒惑色"; ObjectID = "n4b-uW-Wc9"; */ -"n4b-uW-Wc9.title" = "Mars"; - -/* Class = "NSTextFieldCell"; title = "望標記色"; ObjectID = "Nff-aq-7xJ"; */ -"Nff-aq-7xJ.title" = "Full Moon"; - -/* Class = "NSTextFieldCell"; title = "太白色"; ObjectID = "o4y-9D-nbq"; */ -"o4y-9D-nbq.title" = "Venus"; - -/* Class = "NSButtonCell"; title = "今地"; ObjectID = "oDT-Tn-BIi"; */ -"oDT-Tn-BIi.title" = "Here"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "OGK-lU-Z45"; */ -"OGK-lU-Z45.title" = "Text Cell"; - -/* Class = "NSTextFieldCell"; title = "高"; ObjectID = "OnC-JD-O64"; */ -"OnC-JD-O64.title" = "Height"; - -/* Class = "NSTextFieldCell"; title = "圓角比例"; ObjectID = "or0-dh-Jpl"; */ -"or0-dh-Jpl.title" = "Corner Radius Ratio"; - -/* Class = "NSTableColumn"; headerCell.title = "Modified Date"; ObjectID = "PuJ-IQ-jAy"; */ -"PuJ-IQ-jAy.headerCell.title" = "Modified Date"; - -/* Class = "NSTextFieldCell"; title = "大字色"; ObjectID = "pW5-9q-wEF"; */ -"pW5-9q-wEF.title" = "Center Text Color"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʺ"; ObjectID = "QaI-d6-JyU"; */ -"QaI-d6-JyU.placeholderString" = "0ʺ"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "R3e-Qn-ORj"; */ -"R3e-Qn-ORj.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; title = "小刻透明"; ObjectID = "RHJ-aL-5K8"; */ -"RHJ-aL-5K8.title" = "Minor Tick Transparency"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "rj0-tq-h3X"; */ -"rj0-tq-h3X.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "RYA-Tu-p83"; */ -"RYA-Tu-p83.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; title = "月圈色"; ObjectID = "scY-Ao-XcE"; */ -"scY-Ao-XcE.title" = "Month Ring"; - -/* Class = "NSTextFieldCell"; title = "朔標記色"; ObjectID = "swe-9Q-BIS"; */ -"swe-9Q-BIS.title" = "New Moon"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "t5s-dH-gXO"; */ -"t5s-dH-gXO.title" = "Item 2"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "t9k-ky-sDO"; */ -"t9k-ky-sDO.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʹ"; ObjectID = "TBr-y2-G7r"; */ -"TBr-y2-G7r.placeholderString" = "0ʹ"; - -/* Class = "NSTextFieldCell"; title = "日中色"; ObjectID = "ThU-2N-VNz"; */ -"ThU-2N-VNz.title" = "Solar Noon"; - -/* Class = "NSTableColumn"; headerCell.title = "Theme Name"; ObjectID = "UOo-gj-ZF0"; */ -"UOo-gj-ZF0.headerCell.title" = "Theme Name"; - -/* Class = "NSMenu"; title = "Chinese Time"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "Chinese Time"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "VnC-6l-TXQ"; */ -"VnC-6l-TXQ.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "殘圈透明"; ObjectID = "w3w-Zd-2vt"; */ -"w3w-Zd-2vt.title" = "Inactive Ring Transparency"; - -/* Class = "NSTextFieldCell"; title = "歲星色"; ObjectID = "w9i-IF-diI"; */ -"w9i-IF-diI.title" = "Jupyter"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "wRO-8l-CCP"; */ -"wRO-8l-CCP.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "鎮星色"; ObjectID = "wSA-P1-oPV"; */ -"wSA-P1-oPV.title" = "Saturn"; - -/* Class = "NSTextFieldCell"; title = "月中色"; ObjectID = "XWl-Jf-Ee7"; */ -"XWl-Jf-Ee7.title" = "Lunar Noon"; - -/* Class = "NSTextFieldCell"; title = "氣標記色"; ObjectID = "y52-I9-5nt"; */ -"y52-I9-5nt.title" = "EST Mark"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "yda-3n-YyK"; */ -"yda-3n-YyK.title" = "Item 3"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "yGZ-If-rbI"; */ -"yGZ-If-rbI.title" = "Item 2"; - -/* Class = "NSTableColumn"; headerCell.title = "Device"; ObjectID = "yuf-eE-tHd"; */ -"yuf-eE-tHd.headerCell.title" = "Device"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "zdq-hz-dWJ"; */ -"zdq-hz-dWJ.title" = "Text Cell"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "Zh0-wM-pA4"; */ -"Zh0-wM-pA4.title" = "Item 2"; - -/* Class = "NSTextFieldCell"; title = "大字平移"; ObjectID = "zkW-7g-e4z"; */ -"zkW-7g-e4z.title" = "Center Text H Offset"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "ZPN-2J-EVa"; */ -"ZPN-2J-EVa.title" = "Item 2"; - diff --git a/macOS/layout.txt b/macOS/layout.txt index e41e052..ebad847 100644 --- a/macOS/layout.txt +++ b/macOS/layout.txt @@ -1,19 +1,20 @@ globalMonth: false apparentTime: 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 secondRing: locations: 0.0, 0.25, 0.5, 0.75; colors: 0xFF8658C5, 0xFF6E68E7, 0xFF5283EF, 0xFF6E68E7; loop: true thirdRing: locations: 0.0, 0.25, 0.5, 0.75; colors: 0xFF8D5263, 0xFF9252B0, 0xFF7556EF, 0xFF9252B0; loop: true -innerColor: 0x66FFFFFF +innerColor: 0xFFFFFFFF majorTickColor: 0x00000000 majorTickAlpha: 0.0 minorTickColor: 0x00000000 minorTickAlpha: 0.7 fontColor: 0xFFFFFFFF -centerFontColor: locations: 0.25, 0.75; colors: 0xFF523A6D, 0xFF5A3136; loop: false +centerFontColor: locations: 0.25, 0.75; colors: 0xFFA36497, 0xFFC28F8A; loop: false evenSolarTermTickColor: 0xFF000000 oddSolarTermTickColor: 0xFF555555 -innerColorDark: 0x66FFFFFF +innerColorDark: 0xFF101010 majorTickColorDark: 0x00000000 minorTickColorDark: 0x007F7F7F fontColorDark: 0xFF000000 @@ -27,12 +28,14 @@ evenStermIndicator: 0xFFFFFFFF sunPositionIndicator: 0xFF000000, 0xFFAAADFF, 0xFF0A36B1, 0xFF6EBCD3 moonPositionIndicator: 0xFFE6B8AF, 0xFFE56572, 0xFF9E1E67 shadeAlpha: 0.25 +shadowSize: 0.03 textFont: PingFangTC-Regular centerFont: SourceHanSansKR-Heavy -centerTextOffset: 1.16 -centerTextHorizontalOffset: -0.1 -verticalTextOffset: 0.95 +centerTextOffset: 0.05 +centerTextHorizontalOffset: 0.05 +verticalTextOffset: 0.0 horizontalTextOffset: 0.0 watchWidth: 396.0 watchHeight: 484.0 -cornerRadiusRatio: 0.6 +cornerRadiusRatio: 0.5 +statusBar: date: true, time: true, holiday: 0, separator: space diff --git a/macOS/macApp.swift b/macOS/macApp.swift new file mode 100644 index 0000000..d465d61 --- /dev/null +++ b/macOS/macApp.swift @@ -0,0 +1,126 @@ +// +// AppDelegate.swift +// ChineseTime +// +// Created by LEO Yoon-Tsaw on 9/19/21. +// + +import SwiftUI +import WidgetKit + +@main +struct ChineseTimeMacApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + Welcome() + .frame(minWidth: 300, idealWidth: 350, maxWidth: 400, minHeight: 400, idealHeight: 600, maxHeight: 700, alignment: .center) + } + .windowResizability(.contentSize) + .windowStyle(.hiddenTitleBar) + } +} + +@MainActor +final class AppDelegate: NSObject, NSApplicationDelegate { + static var instance: AppDelegate? + var statusItem: NSStatusItem! + let watchSetting = WatchSetting.shared + let watchLayout = WatchLayout.shared + let modelContainer = ThemeData.container + let locationManager = LocationManager.shared + let chineseCalendar = ChineseCalendar(time: .now) + var watchPanel: WatchPanel! + private var _timer: Timer? + var lastReloaded = Date.distantPast + + func applicationWillFinishLaunching(_ aNotification: Notification) { + statusItem = NSStatusBar.system.statusItem(withLength: 0) + statusItem.button?.action = #selector(self.toggleDisplay(sender:)) + statusItem.button?.sendAction(on: [.leftMouseDown]) + watchLayout.loadDefault(context: modelContainer.mainContext) + locationManager.requestLocation() + AppDelegate.instance = self + } + + func applicationDidFinishLaunching(_ aNotification: Notification) { + update() + watchPanel = { + let watchFace = WatchFace() + .environment(\.chineseCalendar, chineseCalendar) + .modelContainer(modelContainer) + let setting = Setting() + .frame(minWidth: 550, maxWidth: 700, minHeight: 350, maxHeight: 500) + .environment(\.chineseCalendar, chineseCalendar) + .modelContainer(modelContainer) + + return WatchPanelHosting(watch: watchFace, setting: setting, statusItem: statusItem, isPresented: false) + }() + _timer = Timer.scheduledTimer(withTimeInterval: ChineseCalendar.updateInterval, repeats: true) { _ in + Task { @MainActor in + self.update() + } + } + } + + func applicationWillResignActive(_ notification: Notification) { + watchPanel.isPresented = false + } + + func applicationWillTerminate(_ aNotification: Notification) { + watchLayout.saveDefault(context: modelContainer.mainContext) + try? modelContainer.mainContext.save() + if lastReloaded.distance(to: .now) > 1800 { // Half Hour + WidgetCenter.shared.reloadAllTimelines() + } + _timer?.invalidate() + _timer = nil + statusItem = nil + } + + @objc func toggleDisplay(sender: NSStatusItem) { + watchPanel.isPresented.toggle() + if watchPanel.isPresented && lastReloaded.distance(to: .now) > 7200 { // 2 Hours + WidgetCenter.shared.reloadAllTimelines() + lastReloaded = .now + } + } + + func updateStatusBar(dateText: String) { + if let button = statusItem.button { + if dateText.count > 0 { + button.image = nil + button.title = String(dateText.reversed()) + } else { + button.title = "" + let image = NSImage(resource: .image) + image.size = NSMakeSize(NSStatusBar.system.thickness, NSStatusBar.system.thickness) + button.image = image + } + statusItem.length = button.intrinsicContentSize.width + } + } + + func statusBar(from chineseCalendar: ChineseCalendar, options watchLayout: WatchLayout) -> String { + var displayText = [String]() + if watchLayout.statusBar.date { + displayText.append(chineseCalendar.dateString) + } + if watchLayout.statusBar.holiday > 0 { + let holidays = chineseCalendar.holidays + displayText.append(contentsOf: holidays[..= NSMaxX(windowRect) { + frame.origin.x = NSMaxX(windowRect) - frame.width + } else if NSMinX(frame) <= NSMinX(windowRect) { + frame.origin.x = NSMinX(windowRect) + } + setFrame(frame, display: true) + var bounds = contentView!.bounds + bounds.size.height -= buttonSize.height * 1.7 + let shortEdge = min(bounds.width, bounds.height) + let mask = CAShapeLayer() + mask.path = RoundedRect(rect: bounds, nodePos: shortEdge * 0.08, ankorPos: shortEdge * 0.08 * 0.2).path + backView.layer?.mask = mask + bounds.origin.y += buttonSize.height * 1.7 + backView.frame = bounds + settingButton.frame = NSMakeRect(bounds.width / 2 - buttonSize.width * 1.5, buttonSize.height / 2, buttonSize.width, buttonSize.height) + settingButton.button.font = settingButton.button.font?.withSize(buttonSize.height / 2) + closeButton.frame = NSMakeRect(bounds.width / 2 + buttonSize.width * 0.5, buttonSize.height / 2, buttonSize.width, buttonSize.height) + closeButton.button.font = closeButton.button.font?.withSize(buttonSize.height / 2) + } + } + + private func getCurrentScreen() -> NSRect { + var screenRect = NSScreen.main!.frame + let screens = NSScreen.screens + for i in 0 ..< screens.count { + let rect = screens[i].frame + if let statusBarFrame = statusItem.button?.window?.frame, NSPointInRect(NSMakePoint(statusBarFrame.midX, statusBarFrame.midY), rect) { + screenRect = rect + break + } + } + return screenRect + } + + @objc func closeApp(_ sender: NSButton) { + NSApp.terminate(sender) + } +} + +internal final class WatchPanelHosting: WatchPanel { + private let watchView: NSHostingView + private let settingView: NSHostingController + + init(watch: WatchView, setting: SettingView, statusItem: NSStatusItem, isPresented: Bool) { + watchView = NSHostingView(rootView: watch) + settingView = NSHostingController(rootView: setting) + super.init(statusItem: statusItem, isPresented: isPresented) + settingButton.button.action = #selector(openSetting(_:)) + contentView?.addSubview(watchView) + } + + override func panelPosition() { + super.panelPosition() + watchView.frame = backView.frame + } + + @objc func openSetting(_ sender: NSButton) { + if settingWindow == nil { + settingView.sceneBridgingOptions = [.all] + settingWindow = NSWindow(contentViewController: settingView) + settingWindow?.styleMask = [.closable, .resizable, .titled, .unifiedTitleAndToolbar, .fullSizeContentView] + let frame = NSRect(x: frame.minX - 620, y: frame.midY - 200, width: 600, height: 400) + settingWindow?.setFrame(frame, display: true) + let controller = NSWindowController(window: settingWindow) + controller.showWindow(nil) + } else { + settingWindow?.close() + settingWindow = nil + } + } +} + +fileprivate final class OptionView: NSView { + let background: NSVisualEffectView + let button: NSButton + override var frame: NSRect { + didSet { + self.background.frame = self.bounds + self.button.frame = self.bounds + (background.layer?.mask as? CAShapeLayer)?.path = RoundedRect(rect: background.bounds, nodePos: background.bounds.height / 2, ankorPos: background.bounds.height / 2 * 0.2).path + } + } + + override init(frame frameRect: NSRect) { + let background = NSVisualEffectView(frame: frameRect) + background.blendingMode = .behindWindow + background.material = .hudWindow + background.state = .active + background.wantsLayer = true + let optionMask = CAShapeLayer() + optionMask.path = RoundedRect(rect: background.bounds, nodePos: background.bounds.height / 2, ankorPos: background.bounds.height / 2 * 0.2).path + background.layer?.mask = optionMask + + let button = NSButton(frame: frameRect) + button.alignment = .center + button.isBordered = false + + self.background = background + self.button = button + super.init(frame: frameRect) + addSubview(self.background) + addSubview(self.button) + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + } diff --git a/macOS/zh-Hans.lproj/InfoPlist.strings b/macOS/zh-Hans.lproj/InfoPlist.strings deleted file mode 100644 index e966d2d..0000000 --- a/macOS/zh-Hans.lproj/InfoPlist.strings +++ /dev/null @@ -1,15 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "华历"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 协议开源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可计算当前日出入、月出入时刻,不提供不影响使用其它功能。"; - diff --git a/macOS/zh-Hans.lproj/Localizable.strings b/macOS/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index f3a61e4..0000000 --- a/macOS/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,84 +0,0 @@ -/* Cancel button title */ -"Cancel" = "取消"; - -/* Warning */ -"Choose a layout file to load from" = "选要载入的文档"; - -/* Quit without saves error question message */ -"Could not save changes while quitting. Quit anyway?" = "退出前未能保存,退出吗?"; - -/* Default save file name */ -"Default" = "常备"; - -/* Load Failed */ -"Load Failed" = "加载失败"; - -/* Quit anyway button title */ -"Quit anyway" = "仍退出"; - -/* Quit without saves error question info */ -"Quitting now will lose any changes you have made since the last successful save" = "现在退出将丢失未保存的内容"; - -/* Save Failed */ -"Save Failed" = "保存失败"; - -/* Save File */ -"Select File" = "选择文件"; - -/* Open File */ -"Select Layout File" = "选择布局文件"; - -/* Save File */ -"Select Location" = "选位置"; - -/* Warning */ -"Warning: The current layout will be discarded!" = "警告:当前布局尚未保存!"; - -/* no blank, no duplicate name */ -"不得爲空,不得重名" = "不得为空,不得重名"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# 为何要做此华历?\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**五星**之古名与今不同。辰星即今之水星,又单名“星”;太白星即今之金星,以其为天空最亮之星,故名太白;荧惑即今之火星;岁星即今之木星,太岁乃与岁星相关但不同的概念;填星又作镇星,即今之土星。"; - -/* Ok */ -"作罷" = "作罢"; - -/* Confirm to delete theme message */ -"刪:" = "删:"; - -/* Confirm to delete theme title */ -"刪主題" = "删主题"; - -/* set a name */ -"取名" = "取名"; - -/* Confirm Resetting Settings */ -"吾意已決" = "吾意已决"; - -/* Cancel Resetting Settings */ -"容吾三思" = "容吾三思"; - -/* Alert: Location service disabled */ -"怪哉" = "怪哉"; - -/* Confirm to select theme title */ -"換主題" = "换主题"; - -/* Confirm to select theme message */ -"換爲:" = "换为:"; - -/* rename */ -"易名" = "易名"; - -/* new theme default name */ -"無名" = "无名"; - -/* Unknown saved file */ -"神祕檔" = "神秘档"; - -/* Please enable location service to obtain your longitude and latitude */ -"蓋因定位未開啓" = "盖因定位未开启"; - -/* No selection when exporting */ -"選定主題先" = "选定主题先"; - diff --git a/macOS/zh-Hans.lproj/Main.strings b/macOS/zh-Hans.lproj/Main.strings deleted file mode 100644 index 50f4093..0000000 --- a/macOS/zh-Hans.lproj/Main.strings +++ /dev/null @@ -1,300 +0,0 @@ -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "0Bo-Tc-KcV"; */ -"0Bo-Tc-KcV.title" = "Item 1"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "0WZ-lt-4qc"; */ -"0WZ-lt-4qc.title" = "回环"; - -/* Class = "NSMenuItem"; title = "Chinese Time"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "华历"; - -/* Class = "NSButtonCell"; title = "今時"; ObjectID = "3mu-Kk-md7"; */ -"3mu-Kk-md7.title" = "今时"; - -/* Class = "NSWindow"; title = "注釋"; ObjectID = "4qE-Yy-bnn"; */ -"4qE-Yy-bnn.title" = "注释"; - -/* Class = "NSMenuItem"; title = "Quit Chinese Time"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "关闭华历"; - -/* Class = "NSButtonCell"; title = "主題庫"; ObjectID = "5zZ-P4-UFo"; */ -"5zZ-P4-UFo.title" = "主题库"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "6g9-iA-NU6"; */ -"6g9-iA-NU6.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʺ"; ObjectID = "6GB-kh-C3O"; */ -"6GB-kh-C3O.placeholderString" = "0ʺ"; - -/* Class = "NSTextFieldCell"; title = "小字體"; ObjectID = "6nN-DT-d84"; */ -"6nN-DT-d84.title" = "小字体"; - -/* Class = "NSSegmentedCell"; 6Rv-hF-6sh.ibShadowedLabels[0] = "E"; ObjectID = "6Rv-hF-6sh"; */ -"6Rv-hF-6sh.ibShadowedLabels[0]" = "E"; - -/* Class = "NSSegmentedCell"; 6Rv-hF-6sh.ibShadowedLabels[1] = "W"; ObjectID = "6Rv-hF-6sh"; */ -"6Rv-hF-6sh.ibShadowedLabels[1]" = "W"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "7kl-LP-siJ"; */ -"7kl-LP-siJ.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; placeholderString = "0°"; ObjectID = "9wL-YO-Fvc"; */ -"9wL-YO-Fvc.placeholderString" = "0°"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "34d-4a-LV9"; */ -"34d-4a-LV9.title" = "Item 3"; - -/* Class = "NSButtonCell"; title = "畢"; ObjectID = "36y-D0-7sG"; */ -"36y-D0-7sG.title" = "毕"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "88I-Qx-JWn"; */ -"88I-Qx-JWn.title" = "Text Cell"; - -/* Class = "NSTextFieldCell"; title = "大字體"; ObjectID = "327-wT-qfw"; */ -"327-wT-qfw.title" = "大字体"; - -/* Class = "NSTextFieldCell"; title = "中氣色"; ObjectID = "507-LI-4nD"; */ -"507-LI-4nD.title" = "中气色"; - -/* Class = "NSTextFieldCell"; title = "小刻色"; ObjectID = "523-Rm-pQ5"; */ -"523-Rm-pQ5.title" = "小刻色"; - -/* Class = "NSTextFieldCell"; title = "日圈色"; ObjectID = "Acb-cx-xKE"; */ -"Acb-cx-xKE.title" = "日轮色"; - -/* Class = "NSTextFieldCell"; title = "Chinese Time by Leo Liu"; ObjectID = "aMv-K7-khV"; */ -"aMv-K7-khV.title" = "Chinese Time by Leo Liu"; - -/* Class = "NSButtonCell"; title = "畢"; ObjectID = "aOL-ci-PVf"; */ -"aOL-ci-PVf.title" = "毕"; - -/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ -"AYu-sK-qS6.title" = "Main Menu"; - -/* Class = "NSTextFieldCell"; title = "小字平移"; ObjectID = "BGQ-oh-Lvr"; */ -"BGQ-oh-Lvr.title" = "小字平移"; - -/* Class = "NSTextFieldCell"; title = "大字縱移"; ObjectID = "BKn-Hv-5S1"; */ -"BKn-Hv-5S1.title" = "大字纵移"; - -/* Class = "NSTextFieldCell"; title = "辰星色"; ObjectID = "C7q-vR-gUk"; */ -"C7q-vR-gUk.title" = "辰星色"; - -/* Class = "NSButtonCell"; title = "應用"; ObjectID = "CJ3-Yd-8ff"; */ -"CJ3-Yd-8ff.title" = "应用"; - -/* Class = "NSTextFieldCell"; title = "月色"; ObjectID = "cjC-Ct-irg"; */ -"cjC-Ct-irg.title" = "月色"; - -/* Class = "NSSegmentedCell"; Ckq-JL-XzL.ibShadowedLabels[0] = "N"; ObjectID = "Ckq-JL-XzL"; */ -"Ckq-JL-XzL.ibShadowedLabels[0]" = "N"; - -/* Class = "NSSegmentedCell"; Ckq-JL-XzL.ibShadowedLabels[1] = "S"; ObjectID = "Ckq-JL-XzL"; */ -"Ckq-JL-XzL.ibShadowedLabels[1]" = "S"; - -/* Class = "NSTextFieldCell"; title = "大刻透明"; ObjectID = "D0x-kL-iSv"; */ -"D0x-kL-iSv.title" = "大刻透明"; - -/* Class = "NSMenuItem"; title = "精確至日"; ObjectID = "D6Z-91-ZaT"; */ -"D6Z-91-ZaT.title" = "精确至日"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʹ"; ObjectID = "d72-kj-YSj"; */ -"d72-kj-YSj.placeholderString" = "0ʹ"; - -/* Class = "NSTextFieldCell"; title = "大刻色"; ObjectID = "daY-1b-12h"; */ -"daY-1b-12h.title" = "大刻色"; - -/* Class = "NSTextFieldCell"; title = "年圈色"; ObjectID = "dff-rx-wxj"; */ -"dff-rx-wxj.title" = "年轮色"; - -/* Class = "NSTextFieldCell"; title = "夜中色"; ObjectID = "dgh-87-b86"; */ -"dgh-87-b86.title" = "夜中色"; - -/* Class = "NSTextFieldCell"; title = "月出色"; ObjectID = "DNM-Du-Mk0"; */ -"DNM-Du-Mk0.title" = "月出色"; - -/* Class = "NSTextFieldCell"; title = "經緯度"; ObjectID = "DRr-dI-0Vw"; */ -"DRr-dI-0Vw.title" = "经纬度"; - -/* Class = "NSTextFieldCell"; title = "寬"; ObjectID = "Dt7-Kj-jz1"; */ -"Dt7-Kj-jz1.title" = "宽"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "DYi-IC-KBp"; */ -"DYi-IC-KBp.title" = "回环"; - -/* Class = "NSTextFieldCell"; title = "日入色"; ObjectID = "ECg-3b-kMh"; */ -"ECg-3b-kMh.title" = "日入色"; - -/* Class = "NSWindow"; title = "設置"; ObjectID = "ECM-Fk-e9r"; */ -"ECM-Fk-e9r.title" = "设置"; - -/* Class = "NSMenuItem"; title = "標準時"; ObjectID = "F9Y-Uq-Kvt"; */ -"F9Y-Uq-Kvt.title" = "标准时"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "FgB-zA-LTz"; */ -"FgB-zA-LTz.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "內核色"; ObjectID = "fjH-PX-sg1"; */ -"fjH-PX-sg1.title" = "內核色"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "fPn-WR-Q0a"; */ -"fPn-WR-Q0a.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "月入色"; ObjectID = "Gkh-ME-dxX"; */ -"Gkh-ME-dxX.title" = "月入色"; - -/* Class = "NSTextFieldCell"; title = "置閏法"; ObjectID = "gOr-wQ-sKV"; */ -"gOr-wQ-sKV.title" = "置闰法"; - -/* Class = "NSMenuItem"; title = "真太陽時"; ObjectID = "hIW-FP-Vjc"; */ -"hIW-FP-Vjc.title" = "真太阳时"; - -/* Class = "NSTextFieldCell"; title = "顯示時間"; ObjectID = "HSN-Sc-b1q"; */ -"HSN-Sc-b1q.title" = "显示时间"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "Iz4-69-Iua"; */ -"Iz4-69-Iua.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; title = "小字色"; ObjectID = "j24-Kl-HOr"; */ -"j24-Kl-HOr.title" = "小字色"; - -/* Class = "NSTextFieldCell"; title = "小字縱移"; ObjectID = "jVV-K8-M9e"; */ -"jVV-K8-M9e.title" = "小字纵移"; - -/* Class = "NSMenuItem"; title = "精確至時刻"; ObjectID = "jwN-LD-oM5"; */ -"jwN-LD-oM5.title" = "精确至时刻"; - -/* Class = "NSTextFieldCell"; placeholderString = "0°"; ObjectID = "l3A-Gk-ezv"; */ -"l3A-Gk-ezv.placeholderString" = "0°"; - -/* Class = "NSTextFieldCell"; title = "節色"; ObjectID = "l14-gO-pPw"; */ -"l14-gO-pPw.title" = "节色"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "LjG-ay-Nbv"; */ -"LjG-ay-Nbv.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "節標記色"; ObjectID = "loO-Wq-y3o"; */ -"loO-Wq-y3o.title" = "节标记色"; - -/* Class = "NSTextFieldCell"; title = "日出色"; ObjectID = "LV5-B3-f4g"; */ -"LV5-B3-f4g.title" = "日出色"; - -/* Class = "NSButtonCell"; title = "迴環"; ObjectID = "mbO-lM-Hhe"; */ -"mbO-lM-Hhe.title" = "回环"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "mMa-86-JbP"; */ -"mMa-86-JbP.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "時間"; ObjectID = "mWl-tT-NW1"; */ -"mWl-tT-NW1.title" = "时间"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "MYn-FC-mCk"; */ -"MYn-FC-mCk.title" = "Item 2"; - -/* Class = "NSTextFieldCell"; title = "熒惑色"; ObjectID = "n4b-uW-Wc9"; */ -"n4b-uW-Wc9.title" = "荧惑色"; - -/* Class = "NSTextFieldCell"; title = "望標記色"; ObjectID = "Nff-aq-7xJ"; */ -"Nff-aq-7xJ.title" = "望标记色"; - -/* Class = "NSTextFieldCell"; title = "太白色"; ObjectID = "o4y-9D-nbq"; */ -"o4y-9D-nbq.title" = "太白色"; - -/* Class = "NSButtonCell"; title = "今地"; ObjectID = "oDT-Tn-BIi"; */ -"oDT-Tn-BIi.title" = "今地"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "OGK-lU-Z45"; */ -"OGK-lU-Z45.title" = "Text Cell"; - -/* Class = "NSTextFieldCell"; title = "高"; ObjectID = "OnC-JD-O64"; */ -"OnC-JD-O64.title" = "高"; - -/* Class = "NSTextFieldCell"; title = "圓角比例"; ObjectID = "or0-dh-Jpl"; */ -"or0-dh-Jpl.title" = "圆角比例"; - -/* Class = "NSTableColumn"; headerCell.title = "Modified Date"; ObjectID = "PuJ-IQ-jAy"; */ -"PuJ-IQ-jAy.headerCell.title" = "更改日"; - -/* Class = "NSTextFieldCell"; title = "大字色"; ObjectID = "pW5-9q-wEF"; */ -"pW5-9q-wEF.title" = "大字色"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʺ"; ObjectID = "QaI-d6-JyU"; */ -"QaI-d6-JyU.placeholderString" = "0ʺ"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "R3e-Qn-ORj"; */ -"R3e-Qn-ORj.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; title = "小刻透明"; ObjectID = "RHJ-aL-5K8"; */ -"RHJ-aL-5K8.title" = "小刻透明"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "rj0-tq-h3X"; */ -"rj0-tq-h3X.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "RYA-Tu-p83"; */ -"RYA-Tu-p83.title" = "Table View Cell"; - -/* Class = "NSTextFieldCell"; title = "月圈色"; ObjectID = "scY-Ao-XcE"; */ -"scY-Ao-XcE.title" = "月轮色"; - -/* Class = "NSTextFieldCell"; title = "朔標記色"; ObjectID = "swe-9Q-BIS"; */ -"swe-9Q-BIS.title" = "朔标记色"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "t5s-dH-gXO"; */ -"t5s-dH-gXO.title" = "Item 2"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "t9k-ky-sDO"; */ -"t9k-ky-sDO.title" = "Item 3"; - -/* Class = "NSTextFieldCell"; placeholderString = "0ʹ"; ObjectID = "TBr-y2-G7r"; */ -"TBr-y2-G7r.placeholderString" = "0ʹ"; - -/* Class = "NSTextFieldCell"; title = "日中色"; ObjectID = "ThU-2N-VNz"; */ -"ThU-2N-VNz.title" = "日中色"; - -/* Class = "NSTableColumn"; headerCell.title = "Theme Name"; ObjectID = "UOo-gj-ZF0"; */ -"UOo-gj-ZF0.headerCell.title" = "主题名"; - -/* Class = "NSMenu"; title = "Chinese Time"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "华历"; - -/* Class = "NSMenuItem"; title = "Item 1"; ObjectID = "VnC-6l-TXQ"; */ -"VnC-6l-TXQ.title" = "Item 1"; - -/* Class = "NSTextFieldCell"; title = "殘圈透明"; ObjectID = "w3w-Zd-2vt"; */ -"w3w-Zd-2vt.title" = "残圈透明"; - -/* Class = "NSTextFieldCell"; title = "歲星色"; ObjectID = "w9i-IF-diI"; */ -"w9i-IF-diI.title" = "岁星色"; - -/* Class = "NSTextFieldCell"; title = "1.0"; ObjectID = "wRO-8l-CCP"; */ -"wRO-8l-CCP.title" = "1.0"; - -/* Class = "NSTextFieldCell"; title = "鎮星色"; ObjectID = "wSA-P1-oPV"; */ -"wSA-P1-oPV.title" = "镇星色"; - -/* Class = "NSTextFieldCell"; title = "月中色"; ObjectID = "XWl-Jf-Ee7"; */ -"XWl-Jf-Ee7.title" = "月中色"; - -/* Class = "NSTextFieldCell"; title = "氣標記色"; ObjectID = "y52-I9-5nt"; */ -"y52-I9-5nt.title" = "气标记色"; - -/* Class = "NSMenuItem"; title = "Item 3"; ObjectID = "yda-3n-YyK"; */ -"yda-3n-YyK.title" = "Item 3"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "yGZ-If-rbI"; */ -"yGZ-If-rbI.title" = "Item 2"; - -/* Class = "NSTableColumn"; headerCell.title = "Device"; ObjectID = "yuf-eE-tHd"; */ -"yuf-eE-tHd.headerCell.title" = "器械"; - -/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "zdq-hz-dWJ"; */ -"zdq-hz-dWJ.title" = "Text Cell"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "Zh0-wM-pA4"; */ -"Zh0-wM-pA4.title" = "Item 2"; - -/* Class = "NSTextFieldCell"; title = "大字平移"; ObjectID = "zkW-7g-e4z"; */ -"zkW-7g-e4z.title" = "大字平移"; - -/* Class = "NSMenuItem"; title = "Item 2"; ObjectID = "ZPN-2J-EVa"; */ -"ZPN-2J-EVa.title" = "Item 2"; - diff --git a/macOS/zh-Hant.lproj/InfoPlist.strings b/macOS/zh-Hant.lproj/InfoPlist.strings deleted file mode 100644 index 3002732..0000000 --- a/macOS/zh-Hant.lproj/InfoPlist.strings +++ /dev/null @@ -1,15 +0,0 @@ -/* Bundle name */ -"CFBundleName" = "華曆"; - -/* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "以 GPL v3 協議開源"; - -/* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - -/* Privacy - Location Usage Description */ -"NSLocationUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - -/* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "提供定位可計算當前日出入、月出入時刻,不提供不影響使用其它功能。"; - diff --git a/macOS/zh-Hant.lproj/Localizable.strings b/macOS/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index d20f92e..0000000 --- a/macOS/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,39 +0,0 @@ -/* Cancel button title */ -"Cancel" = "取消"; - -/* Warning */ -"Choose a layout file to load from" = "選要載入的檔案"; - -/* Quit without saves error question message */ -"Could not save changes while quitting. Quit anyway?" = "退出前未能保存,退出嗎?"; - -/* Default save file name */ -"Default" = "常備"; - -/* Load Failed */ -"Load Failed" = "加載失敗"; - -/* Quit anyway button title */ -"Quit anyway" = "仍退出"; - -/* Quit without saves error question info */ -"Quitting now will lose any changes you have made since the last successful save" = "現在退出將丢失未保存的內容"; - -/* Save Failed */ -"Save Failed" = "保存失敗"; - -/* Save File */ -"Select File" = "選擇文檔"; - -/* Open File */ -"Select Layout File" = "選擇佈局文檔"; - -/* Save File */ -"Select Location" = "選位置"; - -/* Warning */ -"Warning: The current layout will be discarded!" = "警告:當前佈局尚未保存!"; - -/* Markdown formatted Wiki */ -"介紹全文" = "# 爲何要做此華曆?\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**五星**之古名與今不同。辰星即今之水星,又單名「星」;太白星即今之金星,以其爲天空最亮之星,故名太白;熒惑即今之火星;歲星即今之木星,太歲乃與歲星相關但不同的概念;填星又作鎮星,即今之土星。"; - diff --git a/macOS/zh-Hant.lproj/Main.strings b/macOS/zh-Hant.lproj/Main.strings deleted file mode 100644 index 806de6c..0000000 --- a/macOS/zh-Hant.lproj/Main.strings +++ /dev/null @@ -1,30 +0,0 @@ -/* Class = "NSMenuItem"; title = "Chinese Time"; ObjectID = "1Xt-HY-uBw"; */ -"1Xt-HY-uBw.title" = "華曆"; - -/* Class = "NSMenuItem"; title = "Quit Chinese Time"; ObjectID = "4sb-4s-VLi"; */ -"4sb-4s-VLi.title" = "關閉華曆"; - -/* Class = "NSTextFieldCell"; title = "日圈色"; ObjectID = "Acb-cx-xKE"; */ -"Acb-cx-xKE.title" = "日輪色"; - -/* Class = "NSTextFieldCell"; title = "年圈色"; ObjectID = "dff-rx-wxj"; */ -"dff-rx-wxj.title" = "年輪色"; - -/* Class = "NSTextFieldCell"; title = "子夜色"; ObjectID = "dgh-87-b86"; */ -"dgh-87-b86.title" = "夜中色"; - -/* Class = "NSTableColumn"; headerCell.title = "Modified Date"; ObjectID = "PuJ-IQ-jAy"; */ -"PuJ-IQ-jAy.headerCell.title" = "更改日"; - -/* Class = "NSTextFieldCell"; title = "月圈色"; ObjectID = "scY-Ao-XcE"; */ -"scY-Ao-XcE.title" = "月輪色"; - -/* Class = "NSTableColumn"; headerCell.title = "Theme Name"; ObjectID = "UOo-gj-ZF0"; */ -"UOo-gj-ZF0.headerCell.title" = "主題名"; - -/* Class = "NSMenu"; title = "Chinese Time"; ObjectID = "uQy-DD-JDr"; */ -"uQy-DD-JDr.title" = "華曆"; - -/* Class = "NSTableColumn"; headerCell.title = "Device"; ObjectID = "yuf-eE-tHd"; */ -"yuf-eE-tHd.headerCell.title" = "器械"; -