From 1d632abca88fcd2f835a412a2586ff377db1b3b6 Mon Sep 17 00:00:00 2001 From: LEO Yoon-Tsaw Date: Fri, 5 Apr 2024 18:08:15 -0400 Subject: [PATCH] Version 5.4 1. Now you can manage multiple clocks, switch between different timezones at ease 2. Starting angle of Rings are now customizable 3. Long press on a theme now shows a preview on a theme 4. Refactor code to remove shared objects 5. Bug fix bug fix 123 --- .periphery.yml | 1 + Chinendar.xcodeproj/project.pbxproj | 146 +++---- MacWidget/Info.plist | 2 - .../DataModel/{Model.swift => Calendar.swift} | 195 ++++++--- .../{MetaLayout.swift => DataClass.swift} | 378 +++++++++++++---- .../{ThemeData.swift => DataModel.swift} | 246 +++++++---- Shared/DataModel/LocationManager.swift | 43 +- Shared/Environments.swift | 47 --- Shared/Localizable.xcstrings | 305 ++++++++++++-- Shared/Setting/CalendarConfig.swift | 350 ++++++++++++++++ Shared/Setting/ColorSetting.swift | 12 +- Shared/Setting/Datetime.swift | 87 ++-- Shared/Setting/Decoration.swift | 18 +- Shared/Setting/Documentation.swift | 6 +- Shared/Setting/LayoutSetting.swift | 12 +- Shared/Setting/Location.swift | 69 ++-- Shared/Setting/RingSetting.swift | 22 +- Shared/Setting/StatusState.swift | 8 +- Shared/Setting/ThemesList.swift | 381 +++++++++--------- Shared/Views/HoverView.swift | 5 +- Shared/Views/Icon.swift | 27 +- Shared/Views/SwiftUIUtilities.swift | 29 -- Shared/Views/WatchFaceBasics.swift | 12 +- Shared/Views/WatchFaceView.swift | 23 +- Shared/WatchConnectivity.swift | 54 ++- Vision/Layout.swift | 18 +- Vision/Views/Setting.swift | 27 +- Vision/Views/WatchFace.swift | 18 +- Vision/Views/Welcome.swift | 24 +- Vision/layout.txt | 1 + Vision/visionApp.swift | 44 +- Watch/Layout.swift | 15 +- Watch/Views/ContentView.swift | 26 +- Watch/Views/DateTimeAdjust.swift | 12 +- Watch/Views/Setting.swift | 29 +- Watch/Views/SwitchConfig.swift | 118 ++++++ Watch/Views/WatchFace.swift | 48 ++- Watch/layout.txt | 1 + Watch/watchApp.swift | 38 +- Widget/Dual.swift | 19 +- Widget/Full.swift | 15 +- Widget/Protocols.swift | 22 +- Widget/Single.swift | 20 +- Widget/TaskGroup.swift | 51 ++- Widget/WatchWidgets/Card.swift | 27 +- Widget/WatchWidgets/Circular.swift | 41 +- Widget/WatchWidgets/Corner.swift | 4 + Widget/WatchWidgets/CountDown.swift | 8 +- Widget/WatchWidgets/Relevance.swift | 13 +- Widget/WatchWidgets/TextDesp.swift | 36 +- Widget/WatchWidgets/WatchWidgetBasic.swift | 26 +- iOS/Layout.swift | 29 +- iOS/Views/Setting.swift | 61 ++- iOS/Views/WatchFace.swift | 32 +- iOS/Views/Welcome.swift | 24 +- iOS/iOSApp.swift | 40 +- iOS/layout.txt | 1 + macOS/Info.plist | 2 - macOS/Layout.swift | 18 +- macOS/Views/Setting.swift | 35 +- macOS/Views/WatchFace.swift | 15 +- macOS/Views/Welcome.swift | 87 ++-- macOS/layout.txt | 1 + macOS/macApp.swift | 48 ++- macOS/watchPanel.swift | 9 +- 65 files changed, 2477 insertions(+), 1104 deletions(-) rename Shared/DataModel/{Model.swift => Calendar.swift} (93%) rename Shared/DataModel/{MetaLayout.swift => DataClass.swift} (57%) rename Shared/DataModel/{ThemeData.swift => DataModel.swift} (60%) delete mode 100644 Shared/Environments.swift create mode 100644 Shared/Setting/CalendarConfig.swift delete mode 100644 Shared/Views/SwiftUIUtilities.swift create mode 100644 Watch/Views/SwitchConfig.swift diff --git a/.periphery.yml b/.periphery.yml index d10e0bf..5e264a0 100644 --- a/.periphery.yml +++ b/.periphery.yml @@ -1,5 +1,6 @@ project: Chinendar.xcodeproj retain_objc_accessible: true +retain_public: true schemes: - Chinendar Mac - Chinendar Vision diff --git a/Chinendar.xcodeproj/project.pbxproj b/Chinendar.xcodeproj/project.pbxproj index bcc5b10..b7b5180 100644 --- a/Chinendar.xcodeproj/project.pbxproj +++ b/Chinendar.xcodeproj/project.pbxproj @@ -26,16 +26,13 @@ 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 */; }; + 9EBFBE332A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + 9EBFBE342A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + 9EBFBE352A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + 9EBFBE362A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + 9EBFBE372A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + 9EBFBE382A58A40900DC42AF /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.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 */; }; @@ -56,10 +53,6 @@ B32243E82A0D8E5000E7AED5 /* WatchWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243E72A0D8E5000E7AED5 /* WatchWidgetBundle.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 */; }; @@ -70,7 +63,6 @@ B328A2EF2A3D19A4002191F4 /* ThemesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B328A2EE2A3D19A4002191F4 /* ThemesList.swift */; }; B329909B296A1F7F00D246E9 /* layout.txt in Resources */ = {isa = PBXBuildFile; fileRef = B329909A296A1F7F00D246E9 /* layout.txt */; }; 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 */; }; @@ -91,11 +83,11 @@ 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 */; }; - B3515D0129F616A200E6BCDC /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; + B3515D0129F616A200E6BCDC /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B356C9042B04460A0017EF03 /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B356C9032B04460A0017EF03 /* WatchFace.swift */; }; 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 */; }; + B37063A329FAFF3300CC6E57 /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; + B37063A429FAFF3300CC6E57 /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; B38387A92B08319500A04588 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38387A82B08319500A04588 /* Welcome.swift */; }; B383A6CE2A4D02D8002FADCF /* Single.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6CD2A4D02D8002FADCF /* Single.swift */; }; B383A6CF2A4D02D8002FADCF /* Single.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383A6CD2A4D02D8002FADCF /* Single.swift */; }; @@ -108,10 +100,10 @@ B38B52A22B14C5770055569E /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38B52A12B14C5770055569E /* StatusState.swift */; }; B38B52A32B14C5770055569E /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38B52A12B14C5770055569E /* StatusState.swift */; }; B38CC0492A4F1F1600F4DB9F /* WatchFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38CC0482A4F1F1600F4DB9F /* WatchFace.swift */; }; - B38E96B12A0D3A82002FD662 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; + B38E96B12A0D3A82002FD662 /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B38E96B32A0D3A8A002FD662 /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B38E96B42A0D3A8C002FD662 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; - B38E96B62A0D3AA0002FD662 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; + B38E96B62A0D3AA0002FD662 /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.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 */; }; @@ -119,26 +111,24 @@ B39086122A0317CB00943F2B /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; B39086132A0317CB00943F2B /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; B39086142A0317CB00943F2B /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; - B39086152A0344E500943F2B /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; + B39086152A0344E500943F2B /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B39086162A0344EA00943F2B /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B39086172A0344ED00943F2B /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; - B39086182A0347DE00943F2B /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; + B39086182A0347DE00943F2B /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; 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 */; }; B3970CF32A45066C0095F561 /* TextDesp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BF22A0C7E300063DE44 /* TextDesp.swift */; }; B39B904F2B0178CC0083D05A /* HoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */; }; - B39B90502B0178DF0083D05A /* ThemeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* ThemeData.swift */; }; - B39B90512B0178E90083D05A /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; + B39B90502B0178DF0083D05A /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBFBE322A58A40900DC42AF /* DataModel.swift */; }; + B39B90512B0178E90083D05A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B39B90522B0178EC0083D05A /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B39B90532B0178EE0083D05A /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; B39B90542B0178F20083D05A /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32999282A4F9B7B00B71579 /* LocationManager.swift */; }; - B39B90552B0178F50083D05A /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; + B39B90552B0178F50083D05A /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; B39B90562B0179440083D05A /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; B39B90572B0179470083D05A /* WatchFaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */; }; - B39B90582B01794D0083D05A /* SwiftUIUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */; }; B39B90592B0179500083D05A /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; - B39B905A2B0179580083D05A /* Environments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E90D7EE2A9EABD100855F2C /* Environments.swift */; }; B39B905B2B0179680083D05A /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0438C72A8FD5D7007217A8 /* Locale.swift */; }; B39B905C2B0179700083D05A /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34DA20829FDC0B200562449 /* Utilities.swift */; }; B39B905E2B0179B70083D05A /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B905D2B0179B70083D05A /* Layout.swift */; }; @@ -154,6 +144,7 @@ B39B9DC62B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; B39B9DC72B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; B39B9DC82B57578300D29D60 /* Decoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39B9DC52B57578300D29D60 /* Decoration.swift */; }; + B3ADFCF12BB8CF4A00463CA2 /* SwitchConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3ADFCF02BB8CF4A00463CA2 /* SwitchConfig.swift */; }; B3AE9B0D2B68924300958D58 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AE9B0C2B68924300958D58 /* Icon.swift */; }; B3AE9B0E2B68924300958D58 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AE9B0C2B68924300958D58 /* Icon.swift */; }; B3AE9B0F2B68924300958D58 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AE9B0C2B68924300958D58 /* Icon.swift */; }; @@ -161,7 +152,6 @@ 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 */; }; B3C68B192B5DDC4B00FC08E3 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C68B182B5DDC4B00FC08E3 /* Card.swift */; }; @@ -173,10 +163,10 @@ B3CC8BA12A0B30BC0063DE44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3CC8BA02A0B30BC0063DE44 /* Assets.xcassets */; }; B3CC8BA72A0B30BC0063DE44 /* Chinendar Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3CC8B972A0B30BB0063DE44 /* Chinendar Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B3CC8BAF2A0B31B10063DE44 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3515CE529F6149500E6BCDC /* Layout.swift */; }; - B3CC8BB02A0B31B70063DE44 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Model.swift */; }; + B3CC8BB02A0B31B70063DE44 /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B3CC8BB12A0B31B90063DE44 /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B3CC8BB22A0B31BB0063DE44 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; - B3CC8BB32A0B31BE0063DE44 /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; + B3CC8BB32A0B31BE0063DE44 /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; B3CC8BB42A0B31C20063DE44 /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39086112A0317CB00943F2B /* RoundedRect.swift */; }; B3CC8BB52A0B323E0063DE44 /* WatchFaceBasics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36D2F782A047A2000005162 /* WatchFaceBasics.swift */; }; B3CC8BB72A0B330C0063DE44 /* Full.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC8BB62A0B330C0063DE44 /* Full.swift */; }; @@ -191,10 +181,10 @@ B3E1D6D12A0AB43E00F2905A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3E1D6D02A0AB43E00F2905A /* Assets.xcassets */; }; B3E1D6D82A0AB43E00F2905A /* Chinendar Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B3E1D6C62A0AB43E00F2905A /* Chinendar 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 */; }; + B3E1D6DE2A0AC89300F2905A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; B3E1D6DF2A0AC89600F2905A /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; B3E1D6E02A0AC89800F2905A /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F0825C26FAB23500ADBE13 /* Data.swift */; }; - B3E1D6E12A0AC8E500F2905A /* MetaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* MetaLayout.swift */; }; + B3E1D6E12A0AC8E500F2905A /* DataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37063A229FAFF3300CC6E57 /* DataClass.swift */; }; 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 */; }; @@ -209,12 +199,16 @@ 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 */; }; + B3EA6C1C2BB78A2100FC7D07 /* WatchConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */; }; + B3EA6C212BB79A2600FC7D07 /* CalendarConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EA6C1D2BB78D5A00FC7D07 /* CalendarConfig.swift */; }; + B3EA6C222BB79A2700FC7D07 /* CalendarConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EA6C1D2BB78D5A00FC7D07 /* CalendarConfig.swift */; }; + B3EA6C232BB79A2700FC7D07 /* CalendarConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3EA6C1D2BB78D5A00FC7D07 /* CalendarConfig.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 */; }; + D245D60926FA886200A89044 /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245D60826FA886200A89044 /* Calendar.swift */; }; D26CF1C026FD0C8D004EE9BB /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26CF1BF26FD0C8D004EE9BB /* Layout.swift */; }; D2CFF74D270FF940000CECDA /* PlanetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFF74C270FF940000CECDA /* PlanetModel.swift */; }; D2E4E0E626F7C73E002F3716 /* macApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E4E0E526F7C73E002F3716 /* macApp.swift */; }; @@ -315,9 +309,8 @@ 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 = ""; }; - 9EBFBE322A58A40900DC42AF /* ThemeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ThemeData.swift; path = Shared/DataModel/ThemeData.swift; sourceTree = SOURCE_ROOT; }; + 9EBFBE322A58A40900DC42AF /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DataModel.swift; path = Shared/DataModel/DataModel.swift; sourceTree = SOURCE_ROOT; }; B301073C2A0999A900D0A50C /* SourceHanSansKR-Heavy.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceHanSansKR-Heavy.otf"; sourceTree = ""; }; B301073F2A099A0700D0A50C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B30CF75D2AF827A300B100CF /* Chinendar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chinendar.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -327,7 +320,6 @@ 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 /* 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 = ""; }; @@ -351,7 +343,7 @@ B3515D0629F6189F00E6BCDC /* Chinendar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Chinendar.entitlements; sourceTree = ""; }; B356C9032B04460A0017EF03 /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; 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 = ""; }; + B37063A229FAFF3300CC6E57 /* DataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClass.swift; sourceTree = ""; }; B38387A82B08319500A04588 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; 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 = ""; }; @@ -371,6 +363,7 @@ B39B905F2B017AB10083D05A /* Chinendar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Chinendar.entitlements; sourceTree = ""; }; B39B90612B01809A0083D05A /* layout.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = layout.txt; sourceTree = ""; }; B39B9DC52B57578300D29D60 /* Decoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decoration.swift; sourceTree = ""; }; + B3ADFCF02BB8CF4A00463CA2 /* SwitchConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchConfig.swift; sourceTree = ""; }; B3AE9B0C2B68924300958D58 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; B3BCCEE72A48746000F5745E /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; B3BEB4C22A48994C000751D5 /* WatchFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchFace.swift; sourceTree = ""; }; @@ -398,10 +391,11 @@ 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 = ""; }; + B3EA6C1D2BB78D5A00FC7D07 /* CalendarConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarConfig.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 = ""; }; + D245D60826FA886200A89044 /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.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 /* Chinendar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chinendar.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -469,6 +463,7 @@ children = ( B38B52A12B14C5770055569E /* StatusState.swift */, B328A2EE2A3D19A4002191F4 /* ThemesList.swift */, + B3EA6C1D2BB78D5A00FC7D07 /* CalendarConfig.swift */, B328333B2A46687D00E36989 /* ColorSetting.swift */, B328333D2A4668B000E36989 /* RingSetting.swift */, B39B9DC52B57578300D29D60 /* Decoration.swift */, @@ -507,12 +502,12 @@ B32999272A4F9B5200B71579 /* DataModel */ = { isa = PBXGroup; children = ( - 9EBFBE322A58A40900DC42AF /* ThemeData.swift */, - D245D60826FA886200A89044 /* Model.swift */, + B32999282A4F9B7B00B71579 /* LocationManager.swift */, + 9EBFBE322A58A40900DC42AF /* DataModel.swift */, + B37063A229FAFF3300CC6E57 /* DataClass.swift */, + D245D60826FA886200A89044 /* Calendar.swift */, D2CFF74C270FF940000CECDA /* PlanetModel.swift */, D2F0825C26FAB23500ADBE13 /* Data.swift */, - B32999282A4F9B7B00B71579 /* LocationManager.swift */, - B37063A229FAFF3300CC6E57 /* MetaLayout.swift */, ); path = DataModel; sourceTree = ""; @@ -524,7 +519,6 @@ B3E1D6E32A0ACD7800F2905A /* WatchFaceView.swift */, B3AE9B0C2B68924300958D58 /* Icon.swift */, B3F85EAC2A4A5A0B00F8B40B /* HoverView.swift */, - B32243EF2A0DBEF400E7AED5 /* SwiftUIUtilities.swift */, B39086112A0317CB00943F2B /* RoundedRect.swift */, ); path = Views; @@ -561,7 +555,6 @@ B3BFA2562A05E0590018F99E /* WatchConnectivity.swift */, B34DA20829FDC0B200562449 /* Utilities.swift */, 9E0438C72A8FD5D7007217A8 /* Locale.swift */, - 9E90D7EE2A9EABD100855F2C /* Environments.swift */, B383A6EC2A4D1EA2002FADCF /* Localizable.xcstrings */, B383A6D72A4D1EA1002FADCF /* InfoPlist.xcstrings */, B32999272A4F9B5200B71579 /* DataModel */, @@ -661,6 +654,7 @@ children = ( B3F85EB12A4A5F6900F8B40B /* DateTimeAdjust.swift */, B3F85EB32A4A5FA000F8B40B /* Setting.swift */, + B3ADFCF02BB8CF4A00463CA2 /* SwitchConfig.swift */, B38CC0482A4F1F1600F4DB9F /* WatchFace.swift */, B39086022A0314DD00943F2B /* ContentView.swift */, ); @@ -1002,20 +996,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B39B90552B0178F50083D05A /* MetaLayout.swift in Sources */, + B39B90552B0178F50083D05A /* DataClass.swift in Sources */, B39B90562B0179440083D05A /* WatchFaceBasics.swift in Sources */, B33635262B02FA7B00BA83F7 /* Setting.swift in Sources */, B39B90662B0181D50083D05A /* RingSetting.swift in Sources */, B3AE9B0F2B68924300958D58 /* Icon.swift in Sources */, - B39B905A2B0179580083D05A /* Environments.swift in Sources */, B39B90572B0179470083D05A /* WatchFaceView.swift in Sources */, B38387A92B08319500A04588 /* Welcome.swift in Sources */, B39B906A2B0181E00083D05A /* Location.swift in Sources */, + B3EA6C232BB79A2700FC7D07 /* CalendarConfig.swift in Sources */, B39B905E2B0179B70083D05A /* Layout.swift in Sources */, B39B90652B0181D20083D05A /* ColorSetting.swift in Sources */, - B39B90502B0178DF0083D05A /* ThemeData.swift in Sources */, + B39B90502B0178DF0083D05A /* DataModel.swift in Sources */, B39B9DC82B57578300D29D60 /* Decoration.swift in Sources */, - B39B90582B01794D0083D05A /* SwiftUIUtilities.swift in Sources */, B39B90692B0181DE0083D05A /* Datetime.swift in Sources */, B39B90682B0181DA0083D05A /* LayoutSetting.swift in Sources */, B39B905B2B0179680083D05A /* Locale.swift in Sources */, @@ -1023,7 +1016,7 @@ B38B52A32B14C5770055569E /* StatusState.swift in Sources */, B39B90522B0178EC0083D05A /* PlanetModel.swift in Sources */, B39B905C2B0179700083D05A /* Utilities.swift in Sources */, - B39B90512B0178E90083D05A /* Model.swift in Sources */, + B39B90512B0178E90083D05A /* Calendar.swift in Sources */, B39B90642B0181CE0083D05A /* ThemesList.swift in Sources */, B30CF7642AF827A300B100CF /* visionApp.swift in Sources */, B39B90672B0181D70083D05A /* Documentation.swift in Sources */, @@ -1038,12 +1031,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3515D0129F616A200E6BCDC /* Model.swift in Sources */, + B3515D0129F616A200E6BCDC /* Calendar.swift in Sources */, B328A2EF2A3D19A4002191F4 /* ThemesList.swift in Sources */, B3515CFF29F6169D00E6BCDC /* Data.swift in Sources */, B3BCCEE82A48746000F5745E /* Setting.swift in Sources */, B3BEB4C32A48994C000751D5 /* WatchFace.swift in Sources */, - 9EBFBE342A58A40900DC42AF /* ThemeData.swift in Sources */, + 9EBFBE342A58A40900DC42AF /* DataModel.swift in Sources */, B3515D0029F616A000E6BCDC /* PlanetModel.swift in Sources */, 9E0438C92A8FD5D7007217A8 /* Locale.swift in Sources */, B32833442A46695000E36989 /* Datetime.swift in Sources */, @@ -1056,19 +1049,18 @@ B3AE9B0E2B68924300958D58 /* Icon.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 */, + B37063A429FAFF3300CC6E57 /* DataClass.swift in Sources */, B39B9DC72B57578300D29D60 /* Decoration.swift in Sources */, B3515CF329F6149500E6BCDC /* Layout.swift in Sources */, B3BEB4C52A489A10000751D5 /* WatchFaceView.swift in Sources */, B328333A2A46685200E36989 /* LayoutSetting.swift in Sources */, B32833422A46691800E36989 /* Documentation.swift in Sources */, B39086132A0317CB00943F2B /* RoundedRect.swift in Sources */, - B3BEB4C62A489A99000751D5 /* SwiftUIUtilities.swift in Sources */, B34DA20A29FDC0B200562449 /* Utilities.swift in Sources */, B3BEB4C42A489A0A000751D5 /* WatchFaceBasics.swift in Sources */, + B3EA6C212BB79A2600FC7D07 /* CalendarConfig.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1076,7 +1068,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B32243F02A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B3E1D6E42A0ACD7800F2905A /* WatchFaceView.swift in Sources */, 9E6E10CE2AA6410A004CEDBE /* TaskGroup.swift in Sources */, B3F85EB22A4A5F6900F8B40B /* DateTimeAdjust.swift in Sources */, @@ -1085,7 +1076,7 @@ B39086032A0314DD00943F2B /* ContentView.swift in Sources */, B32243E22A0D3BF600E7AED5 /* Layout.swift in Sources */, B329992F2A4F9C8500B71579 /* LocationManager.swift in Sources */, - B39086182A0347DE00943F2B /* MetaLayout.swift in Sources */, + B39086182A0347DE00943F2B /* DataClass.swift in Sources */, B3BFA2582A05E0590018F99E /* WatchConnectivity.swift in Sources */, B39086172A0344ED00943F2B /* Data.swift in Sources */, B39086012A0314DD00943F2B /* watchApp.swift in Sources */, @@ -1093,10 +1084,10 @@ 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 */, + B39086152A0344E500943F2B /* Calendar.swift in Sources */, + B3ADFCF12BB8CF4A00463CA2 /* SwitchConfig.swift in Sources */, B3F85EB42A4A5FA000F8B40B /* Setting.swift in Sources */, - 9EBFBE352A58A40900DC42AF /* ThemeData.swift in Sources */, + 9EBFBE352A58A40900DC42AF /* DataModel.swift in Sources */, B39086162A0344EA00943F2B /* PlanetModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1105,25 +1096,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3CC8BB32A0B31BE0063DE44 /* MetaLayout.swift in Sources */, + B3CC8BB32A0B31BE0063DE44 /* DataClass.swift in Sources */, B395B5A82A0F22CF003206E7 /* IconView.swift in Sources */, B3C68B192B5DDC4B00FC08E3 /* Card.swift in Sources */, 9E5742812AA504D20052AE70 /* TaskGroup.swift in Sources */, - B3CC8BB02A0B31B70063DE44 /* Model.swift in Sources */, + B3CC8BB02A0B31B70063DE44 /* Calendar.swift in Sources */, B3C68B1C2B5DE90800FC08E3 /* Protocols.swift in Sources */, B34009482A352FEA003F50F7 /* WatchFaceView.swift in Sources */, B3CC8BB12A0B31B90063DE44 /* PlanetModel.swift in Sources */, B383A6CF2A4D02D8002FADCF /* Single.swift in Sources */, B3CC8B9C2A0B30BB0063DE44 /* iOSWidgetBundle.swift in Sources */, - 9EBFBE372A58A40900DC42AF /* ThemeData.swift in Sources */, + 9EBFBE372A58A40900DC42AF /* DataModel.swift in Sources */, B3E8A5192A4CF6BD00302473 /* CountDown.swift in Sources */, B3E8A5162A4CF67700302473 /* Circular.swift in Sources */, B383A6D22A4D02E2002FADCF /* Dual.swift in Sources */, + B3EA6C1C2BB78A2100FC7D07 /* WatchConnectivity.swift in Sources */, 9E0438CC2A8FD5D7007217A8 /* Locale.swift in Sources */, B3CC8BB52A0B323E0063DE44 /* WatchFaceBasics.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 */, @@ -1138,14 +1129,13 @@ buildActionMask = 2147483647; files = ( B3E8A51C2A4CF77000302473 /* Corner.swift in Sources */, - 9EBFBE382A58A40900DC42AF /* ThemeData.swift in Sources */, - B38E96B12A0D3A82002FD662 /* Model.swift in Sources */, + 9EBFBE382A58A40900DC42AF /* DataModel.swift in Sources */, + B38E96B12A0D3A82002FD662 /* Calendar.swift in Sources */, B32243E32A0D3BF600E7AED5 /* Layout.swift in Sources */, B3E8A51A2A4CF6BD00302473 /* CountDown.swift in Sources */, - B38E96B62A0D3AA0002FD662 /* MetaLayout.swift in Sources */, + B38E96B62A0D3AA0002FD662 /* DataClass.swift in Sources */, B395B5A62A0F1A4A003206E7 /* IconView.swift in Sources */, B3C68B1A2B5DDC4B00FC08E3 /* Card.swift in Sources */, - B32243F32A0DBEF400E7AED5 /* SwiftUIUtilities.swift in Sources */, B38E96B42A0D3A8C002FD662 /* Data.swift in Sources */, B32999322A4F9C8700B71579 /* LocationManager.swift in Sources */, B38E96B32A0D3A8A002FD662 /* PlanetModel.swift in Sources */, @@ -1166,9 +1156,8 @@ files = ( 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 */, + B3E1D6E12A0AC8E500F2905A /* DataClass.swift in Sources */, + 9EBFBE362A58A40900DC42AF /* DataModel.swift in Sources */, B3E1D6E02A0AC89800F2905A /* Data.swift in Sources */, B3E1D6E22A0AC91700F2905A /* RoundedRect.swift in Sources */, B3E1D6CE2A0AB43E00F2905A /* MacWidgetBundle.swift in Sources */, @@ -1178,7 +1167,7 @@ B383A6D12A4D02E2002FADCF /* Dual.swift in Sources */, B3CC8BBD2A0B40E00063DE44 /* Full.swift in Sources */, B34009472A352FEA003F50F7 /* WatchFaceView.swift in Sources */, - B3E1D6DE2A0AC89300F2905A /* Model.swift in Sources */, + B3E1D6DE2A0AC89300F2905A /* Calendar.swift in Sources */, B3C68B1E2B5DE94300FC08E3 /* Protocols.swift in Sources */, B3E1D6DF2A0AC89600F2905A /* PlanetModel.swift in Sources */, ); @@ -1191,23 +1180,22 @@ D2CFF74D270FF940000CECDA /* PlanetModel.swift in Sources */, D2F0825D26FAB23500ADBE13 /* Data.swift in Sources */, D2E4E0E626F7C73E002F3716 /* macApp.swift in Sources */, - B37063A329FAFF3300CC6E57 /* MetaLayout.swift in Sources */, + B37063A329FAFF3300CC6E57 /* DataClass.swift in Sources */, B39B9DC62B57578300D29D60 /* Decoration.swift in Sources */, - 9EBFBE332A58A40900DC42AF /* ThemeData.swift in Sources */, - B32999242A4F989600B71579 /* SwiftUIUtilities.swift in Sources */, + 9EBFBE332A58A40900DC42AF /* DataModel.swift in Sources */, + B3EA6C222BB79A2700FC7D07 /* CalendarConfig.swift in Sources */, B32999222A4F96D600B71579 /* Setting.swift in Sources */, 9E0438C82A8FD5D7007217A8 /* Locale.swift in Sources */, B32999292A4F9B7B00B71579 /* LocationManager.swift in Sources */, B38B52A22B14C5770055569E /* StatusState.swift in Sources */, 9ECDCA022A50B24800E11161 /* LayoutSetting.swift in Sources */, 9E71FD072A50BF2E00C9CA78 /* WatchFace.swift in Sources */, - D245D60926FA886200A89044 /* Model.swift in Sources */, + D245D60926FA886200A89044 /* Calendar.swift in Sources */, D26CF1C026FD0C8D004EE9BB /* Layout.swift in Sources */, 9E9889EF2A79EABF0066414A /* WatchPanel.swift in Sources */, B34DA20929FDC0B200562449 /* Utilities.swift in Sources */, 9ECDCA052A50B6D100E11161 /* Documentation.swift in Sources */, B3AE9B0D2B68924300958D58 /* Icon.swift in Sources */, - 9E90D7EF2A9EABD100855F2C /* Environments.swift in Sources */, 9ECDCA032A50B6CB00E11161 /* ColorSetting.swift in Sources */, 9E71FD042A50BD2A00C9CA78 /* Location.swift in Sources */, 9E71FD022A50BB0F00C9CA78 /* ThemesList.swift in Sources */, @@ -1697,8 +1685,8 @@ D2E4E0F326F7C73F002F3716 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - APP_BUILD = 119; - APP_VERSION = 5.3; + APP_BUILD = 131; + APP_VERSION = 5.4; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1777,8 +1765,8 @@ D2E4E0F426F7C73F002F3716 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - APP_BUILD = 119; - APP_VERSION = 5.3; + APP_BUILD = 131; + APP_VERSION = 5.4; ASSETCATALOG_COMPILER_APPICON_NAME = ""; ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = SwiftUI; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; diff --git a/MacWidget/Info.plist b/MacWidget/Info.plist index d40f568..fa111e0 100644 --- a/MacWidget/Info.plist +++ b/MacWidget/Info.plist @@ -4,8 +4,6 @@ ATSApplicationFontsPath . - GroupID - $(TeamIdentifierPrefix)ChineseTime ITSAppUsesNonExemptEncryption LSHasLocalizedDisplayName diff --git a/Shared/DataModel/Model.swift b/Shared/DataModel/Calendar.swift similarity index 93% rename from Shared/DataModel/Model.swift rename to Shared/DataModel/Calendar.swift index f0d34b9..ccc938e 100644 --- a/Shared/DataModel/Model.swift +++ b/Shared/DataModel/Calendar.swift @@ -456,7 +456,7 @@ extension Array { } } -@Observable final class ChineseCalendar: @unchecked Sendable { +@Observable final class ChineseCalendar { struct ChineseDate: Hashable { var month: Int @@ -467,6 +467,40 @@ extension Array { lhs.month == rhs.month && lhs.day == rhs.day && lhs.leap == rhs.leap } } + struct Hour { + enum HourFormat { + case full + case partial(index: Int) + } + var hour: Int + var format: HourFormat + var string: String { + guard (0.. 0 { + str += ChineseCalendar.chinese_numbers[minorTick] + } + return str + } + } static let updateInterval: CGFloat = 14.4 //Seconds static let month_chinese = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "臘月", ] @@ -528,8 +562,11 @@ extension Array { @ObservationIgnored private var _startHour: Date = .distantPast @ObservationIgnored private var _endHour: Date = .distantFuture @ObservationIgnored private var _hourNames: [NamedHour] = [] - @ObservationIgnored private var _hour_string: String = "" - @ObservationIgnored private var _quarter_string: String = "" + @ObservationIgnored private var _hourNamesInCurrentHour: [NamedHour] = [] + @ObservationIgnored private var _subhours: [Date] = [] + @ObservationIgnored private var _subhourMinors: [Date] = [] + @ObservationIgnored private var _hour: Hour = Hour(hour: -1, format: .full) + @ObservationIgnored private var _quarter: SubHour = SubHour(majorTick: -1, minorTick: -1) @ObservationIgnored private let _lock = OSAllocatedUnfairLock() struct NamedHour { @@ -572,22 +609,23 @@ extension Array { var minorTicks = [Double]() } - init(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint? = nil, compact: Bool = false) { + init(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint? = nil, compact: Bool = false, globalMonth: Bool = false, apparentTime: Bool = false, largeHour: Bool = false) { self._compact = compact self._time = time var calendar = Calendar.current calendar.timeZone = timezone ?? calendar.timeZone self._calendar = calendar - self._location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location - self._globalMonth = WatchLayout.shared.globalMonth - self._apparentTime = WatchLayout.shared.apparentTime - self._largeHour = WatchLayout.shared.largeHour + self._location = location + self._globalMonth = globalMonth + self._apparentTime = apparentTime + self._largeHour = largeHour updateYear() updateDate() updateHour() + updateSubHour() } - private init(compact: Bool, time: Date, calendar: Calendar, location: CGPoint?, globalMonth: Bool, apparentTime: Bool, largeHour: Bool, year: Int, year_length: Double, numberOfMonths: Int, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], month: Int, precise_month: Int, leap_month: Int, day: Int, sunTimes: [Date?], moonTimes: [Date?], startHour: Date, endHour: Date, hourNames: [NamedHour], hour_string: String, quarter_string: String) { + private init(compact: Bool, time: Date, calendar: Calendar, location: CGPoint?, globalMonth: Bool, apparentTime: Bool, largeHour: Bool, year: Int, year_length: Double, numberOfMonths: Int, solarTerms: [Date], evenSolarTerms: [Date], oddSolarTerms: [Date], moonEclipses: [Date], fullMoons: [Date], month: Int, precise_month: Int, leap_month: Int, day: Int, sunTimes: [Date?], moonTimes: [Date?], startHour: Date, endHour: Date, subhours: [Date], subhourMinors: [Date], hourNames: [NamedHour], hourNamesInCurrentHour: [NamedHour], hour: Hour, quarter: SubHour) { self._compact = compact self._time = time self._calendar = calendar @@ -611,13 +649,16 @@ extension Array { self._moonTimes = moonTimes self._startHour = startHour self._endHour = endHour + self._subhours = subhours + self._subhourMinors = subhourMinors self._hourNames = hourNames - self._hour_string = hour_string - self._quarter_string = quarter_string + self._hourNamesInCurrentHour = hourNames + self._hour = hour + self._quarter = quarter } var copy: ChineseCalendar { - ChineseCalendar(compact: _compact, time: _time, calendar: _calendar, location: _location, globalMonth: _globalMonth, apparentTime: _apparentTime, largeHour: _largeHour, year: _year, year_length: _year_length, numberOfMonths: _numberOfMonths, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, month: _month, precise_month: _precise_month, leap_month: _leap_month, day: _day, sunTimes: _sunTimes, moonTimes: _moonTimes, startHour: _startHour, endHour: _endHour, hourNames: _hourNames, hour_string: _hour_string, quarter_string: _quarter_string) + ChineseCalendar(compact: _compact, time: _time, calendar: _calendar, location: _location, globalMonth: _globalMonth, apparentTime: _apparentTime, largeHour: _largeHour, year: _year, year_length: _year_length, numberOfMonths: _numberOfMonths, solarTerms: _solarTerms, evenSolarTerms: _evenSolarTerms, oddSolarTerms: _oddSolarTerms, moonEclipses: _moonEclipses, fullMoons: _fullMoons, month: _month, precise_month: _precise_month, leap_month: _leap_month, day: _day, sunTimes: _sunTimes, moonTimes: _moonTimes, startHour: _startHour, endHour: _endHour, subhours: _subhours, subhourMinors: _subhourMinors, hourNames: _hourNames, hourNamesInCurrentHour: _hourNamesInCurrentHour, hour: _hour, quarter: _quarter) } private func updateYear() { @@ -789,9 +830,9 @@ extension Array { } if hour <= _time { if _largeHour { - _hour_string = Self.terrestrial_branches[(hourIndex /% 2) %% 12] + _hour = Hour(hour: (hourIndex /% 2) %% 12, format: .full) } else { - _hour_string = Self.terrestrial_branches[((hourIndex + 1) /% 2) %% 12] + Self.sub_hour_name[(hourIndex + 1) %% 2] + _hour = Hour(hour: ((hourIndex + 1) /% 2) %% 12, format: .partial(index: (hourIndex + 1) %% 2)) } } let changeOfHour = if _largeHour { @@ -816,8 +857,51 @@ extension Array { _startHour = tempStartHour! _endHour = tempEndHour! } + + private func updateSubHour() { + _hourNamesInCurrentHour = [] + _subhours = [] + _subhourMinors = [] + + var currentSmallHour = startHour + for namedHour in _hourNames { + if !namedHour.longName.isEmpty && namedHour.hour >= startHour && namedHour.hour < endHour { + _hourNamesInCurrentHour.append(namedHour) + if namedHour.hour <= _time { + currentSmallHour = namedHour.hour + } + } + } + + var majorTickCount = 0 + var tickTime = startOfDay - 864 * 6 + var currentSubhour = currentSmallHour + while tickTime < endHour - 16 { + if tickTime > startHour + 16 { + _subhours.append(tickTime) + } + if tickTime > currentSmallHour && time >= tickTime { + currentSubhour = tickTime + majorTickCount += 1 + } + tickTime += 864 + } - func update(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint? = nil) { + var minorTickCount = 0 + tickTime = startOfDay - 864 * 6 + while tickTime < endHour { + if tickTime > startHour { + _subhourMinors.append(tickTime) + } + if tickTime > currentSubhour && time >= tickTime { + minorTickCount += 1 + } + tickTime += 144 + } + _quarter = SubHour(majorTick: majorTickCount, minorTick: minorTickCount) + } + + func update(time: Date = .now, timezone: TimeZone? = nil, location: CGPoint?? = Optional(nil), globalMonth: Bool? = nil, apparentTime: Bool? = nil, largeHour: Bool? = nil) { _lock.withLock { let oldTimezone = _calendar.timeZone let oldLocation = _location @@ -825,14 +909,14 @@ extension Array { let oldApparentTime = _apparentTime _time = time _calendar.timeZone = timezone ?? _calendar.timeZone - _location = location ?? LocationManager.shared.location ?? WatchLayout.shared.location - _globalMonth = WatchLayout.shared.globalMonth - _apparentTime = WatchLayout.shared.apparentTime - _largeHour = WatchLayout.shared.largeHour + _location = location ?? _location + _globalMonth = globalMonth ?? _globalMonth + _apparentTime = apparentTime ?? _apparentTime + _largeHour = largeHour ?? _largeHour - if (location == nil && oldLocation != nil) || (location != nil && oldLocation == nil) { + if (_location == nil && oldLocation != nil) || (_location != nil && oldLocation == nil) { updateYear() - } else if let newLocation = location, let oldLocation = oldLocation, + } else if let newLocation = _location, let oldLocation = oldLocation, sqrt(pow(newLocation.x - oldLocation.x, 2) + pow(newLocation.y - oldLocation.y, 2)) > 1 { updateYear() } else if timezone != oldTimezone || oldGlobalMonth != _globalMonth || oldApparentTime != _apparentTime { @@ -845,6 +929,7 @@ extension Array { } updateDate() updateHour() + updateSubHour() } } @@ -871,33 +956,30 @@ extension Array { } var timeString: String { + let _hour_string = _hour.string + let _quarter_string = _quarter.string if _hour_string.count < 2 && _hour_string.count + _quarter_string.count < 5 { - "\(_hour_string)時\(_quarter_string)" + return "\(_hour_string)時\(_quarter_string)" } else { - "\(_hour_string)\(_quarter_string)" + return "\(_hour_string)\(_quarter_string)" } } var hourString: String { + let _hour_string = _hour.string if _hour_string.count < 2 { - "\(_hour_string)時" + return "\(_hour_string)時" } else { - _hour_string + return _hour_string } } var quarterString: String { - if _quarter_string.count == 0 { - _ = subhourTicks - } - return _quarter_string + _quarter.string } var shortQuarterString: String { - if _quarter_string.count == 0 { - _ = subhourTicks - } - return _quarter_string.count > 2 ? String(_quarter_string.dropLast(1)) : _quarter_string + _quarter.shortString } var calendar: Calendar { @@ -1109,35 +1191,27 @@ extension Array { } var subhourTicks: Ticks { + let startHour = startHour + let endHour = endHour var ticks = Ticks() var subHourTicks = Set() var majorTickNames = [String]() - var currentSmallHour = startHour - for namedHour in _hourNames { - if !namedHour.longName.isEmpty && namedHour.hour >= startHour && namedHour.hour < endHour { - majorTickNames.append(namedHour.longName) - subHourTicks.insert(startHour.distance(to: namedHour.hour) / startHour.distance(to: endHour)) - if namedHour.hour <= _time { - currentSmallHour = namedHour.hour - } + for namedHour in _hourNamesInCurrentHour { + majorTickNames.append(namedHour.longName) + let distPos = startHour.distance(to: namedHour.hour) / startHour.distance(to: endHour) + if distPos >= 0.0 && distPos < 1.0 { + subHourTicks.insert(distPos) } } let majorTicks = subHourTicks - var majorTickCount = 0 - var tickTime = startOfDay - 864 * 6 - var currentSubhour = currentSmallHour - while tickTime < endHour - 16 { - if tickTime > startHour + 16 { - subHourTicks.insert(startHour.distance(to: tickTime) / startHour.distance(to: endHour)) + for tickTime in _subhours { + let distPos = startHour.distance(to: tickTime) / startHour.distance(to: endHour) + if distPos >= 0.0 && distPos < 1.0 { + subHourTicks.insert(distPos) } - if tickTime > currentSmallHour && time >= tickTime { - currentSubhour = tickTime - majorTickCount += 1 - } - tickTime += 864 } - _quarter_string = Self.chinese_numbers[majorTickCount] + "刻" + let minimumSubhourLength = _compact ? 0.045 : 0.03 var subHourNames = [Ticks.TickName]() var count = 1 @@ -1165,19 +1239,8 @@ extension Array { } var subQuarterTicks = Set() - var minorTickCount = 0 - tickTime = startOfDay - 864 * 6 - while tickTime < endHour { - if tickTime > startHour { - subQuarterTicks.insert(startHour.distance(to: tickTime) / startHour.distance(to: endHour)) - } - if tickTime > currentSubhour && time >= tickTime { - minorTickCount += 1 - } - tickTime += 144 - } - if minorTickCount > 0 { - _quarter_string += Self.chinese_numbers[minorTickCount] + for tickTime in _subhourMinors { + subQuarterTicks.insert(startHour.distance(to: tickTime) / startHour.distance(to: endHour)) } subQuarterTicks = subQuarterTicks.subtracting(subHourTicks) let subQuarterTick = Array(subQuarterTicks).sorted() diff --git a/Shared/DataModel/MetaLayout.swift b/Shared/DataModel/DataClass.swift similarity index 57% rename from Shared/DataModel/MetaLayout.swift rename to Shared/DataModel/DataClass.swift index 63c8dd2..60db57a 100644 --- a/Shared/DataModel/MetaLayout.swift +++ b/Shared/DataModel/DataClass.swift @@ -134,8 +134,36 @@ extension String { } } +fileprivate func extract(from str: String, inner: Bool = false) -> [String: String] { + let regex = if inner { + /([a-zA-Z_0-9]+)\s*:[\s"]*([^\s"#][^"#]*)[\s"#]*(#*.*)$/ + } else { + /^([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 applyGradient(gradient: WatchLayout.Gradient, startingAngle: CGFloat) -> Gradient { + let colors: [CGColor] + let locations: [CGFloat] + if startingAngle >= 0 { + colors = gradient.colors.reversed() + locations = gradient.locations.map { 1 - $0 }.reversed() + } else { + colors = gradient.colors + locations = gradient.locations + } + return Gradient(stops: zip(colors, locations).map { Gradient.Stop(color: Color(cgColor: $0.0), location: $0.1) }) +} + @Observable class MetaWatchLayout { - @Observable final class Gradient: Sendable { + struct Gradient { private let _locations: [CGFloat] private let _colors: [CGColor] let isLoop: Bool @@ -205,13 +233,7 @@ extension String { 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) - } - } + let values = extract(from: str, inner: true) 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 } @@ -221,16 +243,42 @@ extension String { self.isLoop = isLoop } } + + struct StartingPhase { + var zeroRing: CGFloat = 0.0 + var firstRing: CGFloat = 0.0 + var secondRing: CGFloat = 0.0 + var thirdRing: CGFloat = 0.0 + var fourthRing: CGFloat = 0.0 + + init() { } + + func encode() -> String { + var encoded = "" + encoded += "zeroRing: \(zeroRing)\n" + encoded += "firstRing: \(firstRing)\n" + encoded += "secondRing: \(secondRing)\n" + encoded += "thirdRing: \(thirdRing)\n" + encoded += "fourthRing: \(fourthRing)\n" + return encoded + } + + init?(from str: String?) { + guard let str = str else { return nil } + let values = extract(from: str, inner: true) + zeroRing = values["zeroRing"]?.floatValue ?? zeroRing + firstRing = values["firstRing"]?.floatValue ?? firstRing + secondRing = values["secondRing"]?.floatValue ?? secondRing + thirdRing = values["thirdRing"]?.floatValue ?? thirdRing + fourthRing = values["fourthRing"]?.floatValue ?? fourthRing + } + } @ObservationIgnored var initialized = false - var globalMonth: Bool = false - var apparentTime: Bool = false - var largeHour: Bool = false - var locationEnabled: Bool = true - var location: CGPoint? = nil var firstRing = Gradient(locations: [0, 1], colors: [CGColor(gray: 1, alpha: 1), CGColor(gray: 1, alpha: 1)], loop: false) 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 startingPhase = StartingPhase() var innerColor = CGColor(gray: 0, alpha: 0) var backColor = CGColor(gray: 1, alpha: 1) var majorTickColor = CGColor(gray: 0, alpha: 0) @@ -264,17 +312,8 @@ extension String { var watchSize: CGSize = .zero var cornerRadiusRatio: CGFloat = 0 - func encode(includeOffset: Bool = true, includeColor: Bool = true, includeConfig: Bool = true) -> String { + func encode(includeOffset: Bool = true, includeColor: Bool = true) -> String { var encoded = "" - if includeConfig { - encoded += "globalMonth: \(globalMonth)\n" - encoded += "apparentTime: \(apparentTime)\n" - encoded += "largeHour: \(largeHour)\n" - encoded += "locationEnabled: \(locationEnabled)\n" - if let location = location { - encoded += "customLocation: \(location.encode())\n" - } - } if includeColor { encoded += "firstRing: \(firstRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" encoded += "secondRing: \(secondRing.encode().replacingOccurrences(of: "\n", with: "; "))\n" @@ -315,26 +354,15 @@ extension String { encoded += "watchHeight: \(watchSize.height)\n" encoded += "cornerRadiusRatio: \(cornerRadiusRatio)\n" } + encoded += "startingPhase: \(startingPhase.encode().replacingOccurrences(of: "\n", with: "; "))\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], updateSize: Bool = true) { let seperatorRegex = /(\s*;|\{\})/ - func readGradient(value: String?) -> Gradient? { + func expand(value: String?) -> String? { guard let value = value else { return nil } - let newValue = value.replacing(seperatorRegex) { _ in "\n" } - return Gradient(from: newValue) + return value.replacing(seperatorRegex) { _ in "\n" } } func readColorList(_ list: String?) -> [CGColor]? { @@ -350,57 +378,54 @@ extension String { } initialized = true - - globalMonth = values["globalMonth"]?.boolValue ?? globalMonth - apparentTime = values["apparentTime"]?.boolValue ?? apparentTime - largeHour = values["largeHour"]?.boolValue ?? largeHour - locationEnabled = values["locationEnabled"]?.boolValue ?? locationEnabled - location = CGPoint(from: values["customLocation"]) - firstRing = readGradient(value: values["firstRing"]) ?? firstRing - secondRing = readGradient(value: values["secondRing"]) ?? secondRing - thirdRing = readGradient(value: values["thirdRing"]) ?? thirdRing - innerColor = values["innerColor"]?.colorValue ?? innerColor - backColor = values["backColor"]?.colorValue ?? backColor - 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 ?? innerColorDark - backColorDark = values["backColorDark"]?.colorValue ?? backColorDark - majorTickColorDark = values["majorTickColorDark"]?.colorValue ?? majorTickColorDark - minorTickColorDark = values["minorTickColorDark"]?.colorValue ?? minorTickColorDark - fontColorDark = values["fontColorDark"]?.colorValue ?? fontColorDark - 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 updateSize { - if let width = values["watchWidth"]?.floatValue, let height = values["watchHeight"]?.floatValue { - watchSize = CGSize(width: width, height: height) + self.withMutation(keyPath: \.firstRing) { + _firstRing = Gradient(from: expand(value: values["firstRing"])) ?? _firstRing + _secondRing = Gradient(from: expand(value: values["secondRing"])) ?? _secondRing + _thirdRing = Gradient(from: expand(value: values["thirdRing"])) ?? _thirdRing + _startingPhase = StartingPhase(from: expand(value: values["startingPhase"])) ?? _startingPhase + _innerColor = values["innerColor"]?.colorValue ?? _innerColor + _backColor = values["backColor"]?.colorValue ?? _backColor + _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 = Gradient(from: expand(value: values["centerFontColor"])) ?? _centerFontColor + _evenSolarTermTickColor = values["evenSolarTermTickColor"]?.colorValue ?? _evenSolarTermTickColor + _oddSolarTermTickColor = values["oddSolarTermTickColor"]?.colorValue ?? _oddSolarTermTickColor + _innerColorDark = values["innerColorDark"]?.colorValue ?? _innerColorDark + _backColorDark = values["backColorDark"]?.colorValue ?? _backColorDark + _majorTickColorDark = values["majorTickColorDark"]?.colorValue ?? _majorTickColorDark + _minorTickColorDark = values["minorTickColorDark"]?.colorValue ?? _minorTickColorDark + _fontColorDark = values["fontColorDark"]?.colorValue ?? _fontColorDark + _evenSolarTermTickColorDark = values["evenSolarTermTickColorDark"]?.colorValue ?? _evenSolarTermTickColorDark + _oddSolarTermTickColorDark = values["oddSolarTermTickColorDark"]?.colorValue ?? _oddSolarTermTickColorDark + if let colourList = readColorList(values["planetIndicator"]), colourList.count == _planetIndicator.count { + _planetIndicator = colourList + } + if let colourList = readColorList(values["sunPositionIndicator"]), colourList.count == _sunPositionIndicator.count { + _sunPositionIndicator = colourList } + if let colourList = readColorList(values["moonPositionIndicator"]), colourList.count == _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 updateSize { + if let width = values["watchWidth"]?.floatValue, let height = values["watchHeight"]?.floatValue { + _watchSize = CGSize(width: width, height: height) + } + } + _cornerRadiusRatio = values["cornerRadiusRatio"]?.floatValue ?? _cornerRadiusRatio } - cornerRadiusRatio = values["cornerRadiusRatio"]?.floatValue ?? cornerRadiusRatio } func update(from str: String, updateSize: Bool = true) { @@ -418,7 +443,7 @@ extension String { let defaultName = AppInfo.defaultName let predicate = { let deviceName = if local { - try? LocalData.read()?.deviceName ?? AppInfo.deviceName + LocalData.read(context: LocalSchema.context)?.deviceName ?? AppInfo.deviceName } else { AppInfo.deviceName } @@ -449,10 +474,10 @@ extension String { } } - func saveDefault(context: ModelContext) { + @MainActor func saveDefault(context: ModelContext) { let defaultName = AppInfo.defaultName let deviceName = AppInfo.deviceName - try? LocalData.write(deviceName: deviceName) + try? LocalData.write(context: LocalSchema.container.mainContext, deviceName: deviceName) let predicate = #Predicate { data in data.name == defaultName && data.deviceName == deviceName @@ -478,4 +503,179 @@ extension String { context.insert(defaultTheme) } } + + func autoSave() { + withObservationTracking { + _ = self.encode() + } onChange: { + Task { @MainActor in + let context = DataSchema.container.mainContext + self.saveDefault(context: context) + self.autoSave() + } + } + } +} + +@Observable final class CalendarConfigure { + +#if os(iOS) + @ObservationIgnored var watchConnectivity: WatchConnectivityManager? = nil +#endif + @ObservationIgnored var initialized = false + var name: String = AppInfo.defaultName + var globalMonth: Bool = false + var apparentTime: Bool = false + var largeHour: Bool = false + var locationEnabled: Bool = true + var customLocation: CGPoint? = nil + var timezone: TimeZone? = nil + var effectiveTimezone: TimeZone { + timezone ?? Calendar.current.timeZone + } + + convenience init(from code: String, name: String? = nil) { + self.init() + self.update(from: code, newName: name) + } + + func location(locationManager: LocationManager?) -> CGPoint? { + if locationEnabled, let locationManager = locationManager { + return locationManager.location ?? customLocation + } else { + return customLocation + } + } + + func encode(withName: Bool = false) -> String { + var encoded = "" + if withName { + encoded += "name: \(name)\n" + } + encoded += "globalMonth: \(globalMonth)\n" + encoded += "apparentTime: \(apparentTime)\n" + encoded += "largeHour: \(largeHour)\n" + encoded += "locationEnabled: \(locationEnabled)\n" + if let location = customLocation { + encoded += "customLocation: \(location.encode())\n" + } + if let timezone = timezone { + encoded += "timezone: \(timezone.identifier)\n" + } + return encoded + } + + private func update(from values: [String: String], newName: String?) { + initialized = true + self.withMutation(keyPath: \.name) { + _name = newName ?? values["name"] ?? _name + _globalMonth = values["globalMonth"]?.boolValue ?? _globalMonth + _apparentTime = values["apparentTime"]?.boolValue ?? _apparentTime + _largeHour = values["largeHour"]?.boolValue ?? _largeHour + _locationEnabled = values["locationEnabled"]?.boolValue ?? _locationEnabled + _customLocation = CGPoint(from: values["customLocation"]) + if let tzStr = values["timezone"], let tz = TimeZone(identifier: tzStr) { + _timezone = tz + } else { + _timezone = nil + } + } + } + + func update(from str: String, newName: String? = nil) { + let values = extract(from: str) + update(from: values, newName: newName) + } + + func load(name: String?, context: ModelContext) { + let descriptor: FetchDescriptor + if let name = name { + let predicate = #Predicate { data in + data.name == name + } + descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + } else { + descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + } + var found = false + do { + let configs = try context.fetch(descriptor) + for config in configs { + if !found && !config.isNil { + self.update(from: config.code!, newName: config.name!) + found = true + break + } + } + } catch { + print(error.localizedDescription) + } + } + + func save(context: ModelContext) { + let name = name + let predicate = #Predicate { data in + data.name == name + } + let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + var found = false + do { + let configs = try context.fetch(descriptor) + for config in configs { + if !found && !config.isNil { + config.update(code: self.encode(), name: name) + found = true + } else { + context.delete(config) + } + } + } catch { + print(error.localizedDescription) + } + + if !found { + let config = ConfigData(name: self.name, code: self.encode()) + context.insert(config) + } + } + + func sendToWatch() { +#if os(iOS) + self.watchConnectivity?.send(messages: [ + "config": self.encode(withName: true) + ]) +#endif + } + + @MainActor func saveName() { + do { + try LocalData.write(context: LocalSchema.container.mainContext, configName: self.name) + } catch { + print(error.localizedDescription) + } + } + + func autoSaveName() { + withObservationTracking { + _ = self.name + } onChange: { + Task { @MainActor in + self.saveName() + self.autoSaveName() + } + } + } + + func autoSave() { + withObservationTracking { + _ = self.encode(withName: true) + } onChange: { + Task { @MainActor in + let context = DataSchema.container.mainContext + self.save(context: context) + self.sendToWatch() + self.autoSave() + } + } + } } diff --git a/Shared/DataModel/ThemeData.swift b/Shared/DataModel/DataModel.swift similarity index 60% rename from Shared/DataModel/ThemeData.swift rename to Shared/DataModel/DataModel.swift index ac17002..c83d69d 100644 --- a/Shared/DataModel/ThemeData.swift +++ b/Shared/DataModel/DataModel.swift @@ -8,6 +8,7 @@ import Foundation import SwiftData +import CoreData #if os(macOS) import SystemConfiguration #elseif os(iOS) @@ -18,46 +19,58 @@ import WatchKit import VisionKit #endif +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) + } + } +} + struct AppInfo { -#if os(macOS) - static let groupId = Bundle.main.object(forInfoDictionaryKey: "GroupID") as! String -#elseif os(iOS) || os(visionOS) - 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(iOS) || os(visionOS) + static let deviceName = UIDevice.current.name #elseif os(watchOS) static let deviceName = WKInterfaceDevice.current().name -#elseif os(visionOS) - @MainActor static let deviceName = UIDevice.current.name #endif static let defaultName = "__current_theme__" } -typealias ThemeData = DataSchemaV3.Layout -extension ThemeData { - static var version: Int { - intVersion(DataSchemaV3.versionIdentifier) - } - +typealias DataSchema = DataSchemaV4 +extension DataSchema { + static let container = { - let fullSchema = Schema(versionedSchema: DataSchemaV3.self) - let baseUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppInfo.groupId)! #if os(macOS) - let url = baseUrl.appendingPathComponent("ChineseTime") -#else - let url = baseUrl.appendingPathComponent("ChineseTime.sqlite") + let containerPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "28HU5A7B46.ChineseTime")!.appendingPathComponent("ChineseTime") +#elseif os(iOS) || os(visionOS) + let containerPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.ChineseTime")!.appendingPathComponent("ChineseTime.sqlite") +#elseif os(watchOS) + let containerPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.ChineseTime.Watch")!.appendingPathComponent("ChineseTime.sqlite") #endif - let modelConfig = ModelConfiguration("ChineseTime", schema: fullSchema, url: url, cloudKitDatabase: .private("iCloud.YLiu.ChineseTime")) + let fullSchema = Schema(versionedSchema: DataSchema.self) + let modelConfig = ModelConfiguration("ChineseTime", schema: fullSchema, url: containerPath, cloudKitDatabase: .automatic) return createContainer(schema: fullSchema, migrationPlan: DataMigrationPlan.self, configurations: [modelConfig]) }() static let context = ModelContext(container) +} + +typealias ThemeData = DataSchemaV3.Layout +extension ThemeData { + static var version: Int { + intVersion(DataSchema.versionIdentifier) + } static func latestVersion() -> Int { let deviceName = AppInfo.deviceName @@ -66,7 +79,7 @@ extension ThemeData { } var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) descriptor.fetchLimit = 1 - let version = try? context.fetch(descriptor).first?.version + let version = try? DataSchema.context.fetch(descriptor).first?.version return version ?? 0 } static func experienced() -> Bool { @@ -74,11 +87,11 @@ extension ThemeData { data.modifiedDate != nil } var descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.modifiedDate)]) - let counts = try? context.fetchCount(descriptor) + let counts = try? DataSchema.context.fetchCount(descriptor) descriptor.fetchLimit = 1 - let date = try? context.fetch(descriptor).first?.modifiedDate + let date = try? DataSchema.context.fetch(descriptor).first?.modifiedDate - if let date = date, let counts = counts, counts > 1, date.distance(to: .now) > 3600 * 24 * 30 { + if let date = date, let counts = counts, counts > 1, date.distance(to: .now) > 3600 * 24 * 5 { return true } else { return false @@ -100,19 +113,44 @@ extension ThemeData { } } -private func intVersion(_ version: Schema.Version) -> Int { - version.major * 100_0000 + version.minor * 1_0000 + version.patch * 100 + 0 +typealias ConfigData = DataSchemaV4.Config +extension ConfigData { + static var version: Int { + intVersion(DataSchema.versionIdentifier) + } + + var isNil: Bool { + return code == nil || name == nil || modifiedDate == nil + } + + func update(code: String, name: String? = nil) { + if self.code != code { + self.code = code + self.modifiedDate = Date.now + } + if (self.version ?? 0) < Self.version { + self.version = Self.version + } + } } -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 DataSchemaV4: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(2, 0, 0) + static var models: [any PersistentModel.Type] { + [DataSchemaV3.Layout.self, Config.self] + } + + @Model final class Config { + @Attribute(.allowsCloudEncryption) var code: String? + var modifiedDate: Date? + @Attribute(.allowsCloudEncryption) var name: String? + var version: Int? + + init(name: String, code: String) { + self.name = name + self.code = code + self.modifiedDate = Date.now + self.version = intVersion(DataSchemaV4.versionIdentifier) } } } @@ -135,7 +173,7 @@ enum DataSchemaV3: VersionedSchema { self.deviceName = AppInfo.deviceName self.code = code self.modifiedDate = Date.now - self.version = intVersion(DataSchemaV3.versionIdentifier) + self.version = intVersion(DataSchemaV4.versionIdentifier) } } } @@ -186,10 +224,10 @@ enum DataSchemaV1: VersionedSchema { enum DataMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { - [DataSchemaV1.self, DataSchemaV2.self, DataSchemaV3.self] + [DataSchemaV1.self, DataSchemaV2.self, DataSchemaV3.self, DataSchemaV4.self] } - static var stages: [MigrationStage] { [migrateV1toV2, migrateV2toV3] } + static var stages: [MigrationStage] { [migrateV1toV2, migrateV2toV3, migrateV3toV4] } static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self) static let migrateV2toV3 = MigrationStage.custom( @@ -213,77 +251,127 @@ enum DataMigrationPlan: SchemaMigrationPlan { }, didMigrate: nil ) + static let migrateV3toV4 = MigrationStage.lightweight(fromVersion: DataSchemaV3.self, toVersion: DataSchemaV4.self) } -enum LocalSchemaV1: VersionedSchema { - static let versionIdentifier: Schema.Version = .init(1, 1, 0) - static var models: [any PersistentModel.Type] { - [LocalData.self] - } +typealias LocalSchema = LocalSchemaV2 +extension LocalSchema { + + static let container = { + let localSchema = Schema(versionedSchema: LocalSchema.self) + let modelConfig = ModelConfiguration("ChineseTimeLocal", schema: localSchema, groupContainer: .automatic, cloudKitDatabase: .none) + return createContainer(schema: localSchema, migrationPlan: LocalDataMigrationPlan.self, configurations: [modelConfig]) + }() - @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) - } - } + static let context = ModelContext(container) } -typealias LocalData = LocalSchemaV1.LocalData - +typealias LocalData = LocalSchema.LocalData extension LocalData: Identifiable, Hashable { static var version: Int { - intVersion(LocalSchemaV1.versionIdentifier) + intVersion(LocalSchema.versionIdentifier) } - static let container = { - let localSchema = Schema(versionedSchema: LocalSchemaV1.self) - let modelConfig = ModelConfiguration("ChineseTimeLocal", schema: localSchema, groupContainer: .identifier(AppInfo.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 + self.version = Self.version + } + } + + func update(configName: String) { + if self.configName != configName { + self.configName = configName + self.modifiedDate = Date.now + self.version = Self.version } } - static func read() throws -> LocalData? { - let context = LocalData.context + static func read(context: ModelContext) -> LocalData? { let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\LocalData.modifiedDate, order: .reverse)]) - let records = try context.fetch(descriptor) - for record in records { - return record + do { + let records = try context.fetch(descriptor) + for record in records { + return record + } + } catch { + return nil } return nil } - static func write(deviceName: String) throws { - let context = LocalData.context + static func write(context: ModelContext, deviceName: String? = nil, configName: String? = nil) throws { 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) + if let deviceName = deviceName { + record.update(deviceName: deviceName) + } + if let configName = configName { + record.update(configName: configName) + } found = true } else { context.delete(record) } } if !found { - let record = LocalData(deviceName: deviceName) + let record = LocalData(deviceName: deviceName, configName: configName) context.insert(record) } try context.save() } } + + +enum LocalSchemaV2: VersionedSchema { + static let versionIdentifier: Schema.Version = .init(2, 1, 0) + static var models: [any PersistentModel.Type] { + [LocalData.self] + } + + @Model final class LocalData { + var deviceName: String? + var configName: String? + var modifiedDate: Date? + var version: Int? + + init(deviceName: String?, configName: String?) { + self.deviceName = deviceName + self.configName = configName + self.modifiedDate = Date.now + self.version = intVersion(LocalSchemaV2.versionIdentifier) + } + } +} + +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) + } + } +} + +enum LocalDataMigrationPlan: SchemaMigrationPlan { + static var schemas: [any VersionedSchema.Type] { + [LocalSchemaV1.self, LocalSchemaV2.self] + } + + static var stages: [MigrationStage] { [migrateV1toV2] } + static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: LocalSchemaV1.self, toVersion: LocalSchemaV2.self) +} diff --git a/Shared/DataModel/LocationManager.swift b/Shared/DataModel/LocationManager.swift index 5c80c64..4348434 100644 --- a/Shared/DataModel/LocationManager.swift +++ b/Shared/DataModel/LocationManager.swift @@ -9,16 +9,11 @@ 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 - } + _location } set { _location = newValue if newValue == nil { @@ -35,43 +30,39 @@ import Observation var enabled: Bool { get { - if WatchLayout.shared.locationEnabled { - switch manager.authorizationStatus { + switch manager.authorizationStatus { #if os(macOS) - case .authorized, .authorizedAlways: // Location services are available. - return true + case .authorized, .authorizedAlways: // Location services are available. + return true #else - case .authorizedWhenInUse, .authorizedAlways: // Location services are available. - return true + 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 { + case .restricted, .denied: + return false + case .notDetermined: // Authorization not determined yet. + return false + @unknown default: return false } } set { - WatchLayout.shared.locationEnabled = newValue if newValue { requestLocation() + } else { + location = nil } } } - - override private init() { + override init() { super.init() manager.delegate = self manager.requestWhenInUseAuthorization() manager.desiredAccuracy = kCLLocationAccuracyKilometer } - func requestLocation() { - if enabled && (lastUpdated.distance(to: .now) > 3600) { + private func requestLocation() { + if lastUpdated.distance(to: .now) > 3600 { switch manager.authorizationStatus { #if os(macOS) case .authorized, .authorizedAlways: @@ -89,7 +80,7 @@ import Observation } func getLocation() async -> CGPoint? { - if enabled && (lastUpdated.distance(to: .now) > 3600) { + if lastUpdated.distance(to: .now) > 3600 { #if os(watchOS) let authorized = [.authorizedWhenInUse, .authorizedAlways].contains(manager.authorizationStatus) #else diff --git a/Shared/Environments.swift b/Shared/Environments.swift deleted file mode 100644 index 205282d..0000000 --- a/Shared/Environments.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Environments.swift -// Chinendar -// -// 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/Localizable.xcstrings b/Shared/Localizable.xcstrings index a72b1d3..9143976 100644 --- a/Shared/Localizable.xcstrings +++ b/Shared/Localizable.xcstrings @@ -58,6 +58,34 @@ } } }, + "%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } + }, "%@ %@" : { "localizations" : { "en" : { @@ -449,6 +477,34 @@ } } }, + "今時區" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Current Timezone" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今の時間帯" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 시간대" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今时区" + } + } + } + }, "介紹全文" : { "comment" : "Markdown formatted Wiki", "localizations" : { @@ -2082,25 +2138,31 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Export selected theme to file" + "value" : "Export selected content to file" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "設計を紙に書き出す" + "value" : "内容を紙に書き出す" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "종이에 설계을 쓰다" + "value" : "종이에 내용을 쓰다" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "将主题书于纸上" + "value" : "将內容书于纸上" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將內容書於紙上" } } } @@ -2909,6 +2971,70 @@ } } }, + "新功能" : { + "comment" : "Welcome, new features - title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New Features" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新機能" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "신기능" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新功能" + } + } + } + }, + "新增功能詳情" : { + "comment" : "Welcome, new features detail", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save multiple clocks to form time wall, switch at anytime. Starting phase and direction of all rings are now customizable." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在、複数の場所のカレンダーを保存でき、世界中の時間を一覧表示しながら、いつでも切り替えることができます。各輪の開始位置と方向は現在調整可能です。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이제 여러 위치의 캘린더를 저장하고 전 세계의 시간을 한눈에 볼 수 있으며 언제든지 전환할 수 있습니다. 각 바퀴의 시작 위치와 방향을 이제 조정할 수 있습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "现可保存多个地点的日历,一覧世界时间的同时随时切换。各轮起始位置及方向现已可调。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "現可保存多個地點的日曆,一覧世界時間的同時隨時切換。各輪起始位置及方向現已可調。" + } + } + } + }, "日" : { "comment" : "Date", "localizations" : { @@ -3252,6 +3378,35 @@ } } }, + "日曆墻" : { + "comment" : "manage saved configs", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Time Wall" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日暦壁" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시계 벽" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日历墙" + } + } + } + }, "日月光華" : { "localizations" : { "en" : { @@ -3736,6 +3891,34 @@ } } }, + "時輪" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hour Ring" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時輪" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시륜" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时轮" + } + } + } + }, "時辰" : { "localizations" : { "en" : { @@ -4685,6 +4868,34 @@ } } }, + "混沌初開,萬物未成" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Initialized to emptiness" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "混沌の初め、万物未成" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "혼돈의 시작, 만물 미성" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "混沌初开,万物未成" + } + } + } + }, "清明" : { "localizations" : { "en" : { @@ -4859,35 +5070,6 @@ } } }, - "用" : { - "comment" : "Switch to this", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apply" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "施す" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "적용" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "用" - } - } - } - }, "畢" : { "comment" : "Close settings panel", "localizations" : { @@ -6516,6 +6698,35 @@ } } }, + "起始角" : { + "comment" : "Starting Phase", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starting Phase" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最初の角度" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "최초 각도" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "起始角" + } + } + } + }, "距離次事件之倒計時" : { "localizations" : { "en" : { @@ -6775,6 +6986,34 @@ } } }, + "選日曆" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calendar" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日暦を選択" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일력 선택" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选日历" + } + } + } + }, "長按錶盤進設置" : { "comment" : "Welcome, long press - title", "localizations" : { diff --git a/Shared/Setting/CalendarConfig.swift b/Shared/Setting/CalendarConfig.swift new file mode 100644 index 0000000..a4a824e --- /dev/null +++ b/Shared/Setting/CalendarConfig.swift @@ -0,0 +1,350 @@ +// +// CalendarConfig.swift +// Chinendar +// +// Created by Leo Liu on 3/29/24. +// + +import SwiftUI +import SwiftData + +struct ConfigList: View { + @Query(sort: \ConfigData.modifiedDate, order: .reverse) private var configs: [ConfigData] + @Environment(\.modelContext) private var modelContext + @Environment(WatchSetting.self) var watchSetting + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(LocationManager.self) var locationManager + @Environment(ChineseCalendar.self) var chineseCalendar + + @State private var renameAlert = false + @State private var createAlert = false + @State private var deleteAlert = false + @State private var errorAlert = false +#if os(iOS) || os(visionOS) + @State private var isExporting = false + @State private var isImporting = false +#endif + @State private var newName = "" + @State private var errorMsg = "" + @State private var target: ConfigData? = nil + private var invalidName: Bool { + return !validateName(newName) + } + var targetName: String { + if let name = target?.name { + if name == AppInfo.defaultName { + return NSLocalizedString("常用", comment: "") + } else { + return name + } + } else { + return "" + } + } + + var body: some View { + let newConfig = Button { + newName = validName(NSLocalizedString("佚名", comment: "unnamed")) + createAlert = true + } label: { + Label("謄錄", systemImage: "square.and.pencil") + } + + let newConfigConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { + let newConfig = ConfigData(name: newName, code: calendarConfigure.encode()) + modelContext.insert(newConfig) + calendarConfigure.name = newName + do { + try modelContext.save() + } catch { + errorMsg = error.localizedDescription + errorAlert = true + } + } + + let renameConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { + if let target = target, !target.isNil { + let isChangingCurrent = target.name == calendarConfigure.name + target.name = newName + do { + try modelContext.save() + } catch { + errorMsg = error.localizedDescription + errorAlert = true + } + if isChangingCurrent { + calendarConfigure.name = newName + } + self.target = nil + } + } + + let readButton = Button { +#if os(macOS) + readFile(handler: handleFile) +#elseif os(iOS) || os(visionOS) + isImporting = true +#endif + } label: { + Label("讀入", systemImage: "square.and.arrow.down") + } + + let moreMenu = Menu { + VStack { + newConfig + readButton + } + .labelStyle(.titleAndIcon) + } label: { + Label("經理", systemImage: "ellipsis.circle") + } + .menuIndicator(.hidden) + .menuStyle(.automatic) + + Form { + if configs.count > 0 { + Section { +#if os(iOS) + moreMenu + .labelStyle(.titleOnly) + .frame(maxWidth: .infinity) +#endif + ForEach(configs, id: \.self) { config in + if !config.isNil { + let chineseDate: String = { + let calConfig = CalendarConfigure(from: config.code!) + let calendar = ChineseCalendar(time: chineseCalendar.time, + timezone: calConfig.effectiveTimezone, + location: calConfig.location(locationManager: locationManager), + globalMonth: calConfig.globalMonth, apparentTime: calConfig.apparentTime, + largeHour: calConfig.largeHour) + var displayText = [String]() + displayText.append(calendar.dateString) + let holidays = calendar.holidays + displayText.append(contentsOf: holidays[.. Bool { + if name.count > 0 { + return !(configs.map { $0.name }.contains(name)) + } else { + return false + } + } + + func validName(_ name: String) -> String { + var (baseName, i) = reverseNumberedName(name) + while !validateName(numberedName(baseName, number: i)) { + i += 1 + } + return numberedName(baseName, number: i) + } + + func removeDuplicates() { + var records = Set() + for data in configs { + if data.isNil { + modelContext.delete(data) + } else { + if records.contains(data.name!) { + modelContext.delete(data) + } else { + records.insert(data.name!) + } + } + } + } + +#if os(macOS) + @MainActor + func handleFile(_ file: URL) throws { + let configCode = try String(contentsOf: file) + let name = file.lastPathComponent + let namePattern = /^([^\.]+)\.?.*$/ + let configName = try namePattern.firstMatch(in: name)?.output.1 + let config = ConfigData(name: validName(configName != nil ? String(configName!) : name), code: configCode) + modelContext.insert(config) + try modelContext.save() + } +#endif +} + +#Preview("Configs") { + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + + return ConfigList() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(calendarConfigure) + .environment(watchSetting) +} diff --git a/Shared/Setting/ColorSetting.swift b/Shared/Setting/ColorSetting.swift index 1b15395..333af0f 100644 --- a/Shared/Setting/ColorSetting.swift +++ b/Shared/Setting/ColorSetting.swift @@ -24,8 +24,8 @@ struct ColorSettingCell: View { } struct ColorSetting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting var body: some View { Form { @@ -99,5 +99,11 @@ struct ColorSetting: View { } #Preview("Color Setting") { - ColorSetting() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return ColorSetting() + .environment(watchLayout) + .environment(watchSetting) + } diff --git a/Shared/Setting/Datetime.swift b/Shared/Setting/Datetime.swift index edbb872..b9fea5b 100644 --- a/Shared/Setting/Datetime.swift +++ b/Shared/Setting/Datetime.swift @@ -53,18 +53,19 @@ fileprivate struct TimeZoneSelection: Equatable { } } -@MainActor @Observable fileprivate class DateManager { var chineseCalendar: ChineseCalendar? var watchSetting: WatchSetting? - var watchLayout: WatchLayout? + var calendarConfigure: CalendarConfigure? var timeZoneSelection: TimeZoneSelection { get { TimeZoneSelection(timezone: timezone) } set { - watchSetting?.timezone = newValue.timezone - updateTimeZone() + if let tz = newValue.timezone { + calendarConfigure?.timezone = tz + update() + } } } @@ -73,71 +74,78 @@ fileprivate struct TimeZoneSelection: Equatable { watchSetting?.displayTime ?? chineseCalendar?.time ?? .now } set { watchSetting?.displayTime = newValue - updateTime() + update() } } var timezone: TimeZone { get { - watchSetting?.timezone ?? chineseCalendar?.calendar.timeZone ?? Calendar.current.timeZone + calendarConfigure?.timezone ?? chineseCalendar?.calendar.timeZone ?? Calendar.current.timeZone } } var isCurrent: Bool { get { - watchSetting?.displayTime == nil && watchSetting?.timezone == nil + watchSetting?.displayTime == nil } set { if newValue { watchSetting?.displayTime = nil - watchSetting?.timezone = nil } else { watchSetting?.displayTime = chineseCalendar?.time - watchSetting?.timezone = chineseCalendar?.calendar.timeZone } - updateTimeZone() + update() + } + } + + var isTimezoneCurrent: Bool { + get { + calendarConfigure?.timezone == nil + } set { + if newValue { + calendarConfigure?.timezone = nil + } else { + calendarConfigure?.timezone = Calendar.current.timeZone + } + update() } } var globalMonth: Bool { get { - watchLayout?.globalMonth ?? false + calendarConfigure?.globalMonth ?? false } set { - watchLayout?.globalMonth = newValue - updateTime() + calendarConfigure?.globalMonth = newValue + update() } } var apparentTime: Bool { get { - watchLayout?.apparentTime ?? false + calendarConfigure?.apparentTime ?? false } set { - watchLayout?.apparentTime = newValue - updateTime() + calendarConfigure?.apparentTime = newValue + update() } } var largeHour: Bool { get { - watchLayout?.largeHour ?? false + calendarConfigure?.largeHour ?? false } set { - watchLayout?.largeHour = newValue - updateTime() + calendarConfigure?.largeHour = newValue + update() } } - func setup(watchSetting: WatchSetting, watchLayout: WatchLayout, chineseCalendar: ChineseCalendar) { - self.watchLayout = watchLayout + func setup(watchSetting: WatchSetting, calendarConfigure: CalendarConfigure, chineseCalendar: ChineseCalendar) { + self.calendarConfigure = calendarConfigure self.watchSetting = watchSetting self.chineseCalendar = chineseCalendar } - func updateTimeZone() { + private func update() { chineseCalendar?.update(time: watchSetting?.displayTime ?? .now, - timezone: watchSetting?.timezone ?? Calendar.current.timeZone, location: chineseCalendar?.location) - } - - func updateTime() { - chineseCalendar?.update(time: watchSetting?.displayTime ?? .now) + timezone: calendarConfigure?.effectiveTimezone, globalMonth: calendarConfigure?.globalMonth, apparentTime: calendarConfigure?.apparentTime, largeHour: calendarConfigure?.largeHour) } } @@ -154,13 +162,12 @@ private func populateTimezones() -> DataTree { 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 + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(LocationManager.self) var locationManager + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(WatchSetting.self) var watchSetting var body: some View { Form { @@ -179,7 +186,7 @@ struct Datetime: View { HStack { Picker("太陽時", selection: $dateManager.apparentTime) { - let choice = if (locationManager.enabled) || (watchLayout.location != nil) { + let choice = if calendarConfigure.location(locationManager: locationManager) != nil { [true, false] } else { [false] @@ -220,6 +227,7 @@ struct Datetime: View { NSLocalizedString("時區", comment: "Timezone section") } Section(header: Text(timezoneTitle)) { + Toggle("今時區", isOn: $dateManager.isTimezoneCurrent) HStack(spacing: 10) { Picker("大區", selection: $dateManager.timeZoneSelection.primary) { ForEach(dateManager.timeZoneSelection.timeZones.nextLevel.map { $0.nodeName }, id: \.self) { tz in @@ -264,7 +272,7 @@ struct Datetime: View { } .formStyle(.grouped) .task { - dateManager.setup(watchSetting: watchSetting, watchLayout: watchLayout, chineseCalendar: chineseCalendar) + dateManager.setup(watchSetting: watchSetting, calendarConfigure: calendarConfigure, chineseCalendar: chineseCalendar) } .navigationTitle(Text("日時", comment: "Display time settings")) #if os(iOS) @@ -280,5 +288,14 @@ struct Datetime: View { } #Preview("Datetime") { - Datetime() + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + + return Datetime() + .environment(chineseCalendar) + .environment(locationManager) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/Shared/Setting/Decoration.swift b/Shared/Setting/Decoration.swift index 88c76c9..0b03bc6 100644 --- a/Shared/Setting/Decoration.swift +++ b/Shared/Setting/Decoration.swift @@ -28,9 +28,9 @@ struct SliderView: View { return formatter }() Text(formatter.string(from: NSNumber(value: currentValue)) ?? "") - .frame(maxWidth: 40, alignment: .trailing) + .frame(alignment: .trailing) } - Slider(value: $currentValue, in: min...max) { editing in + Slider(value: $currentValue, in: min...max, step: 0.01) { editing in if !editing { value = currentValue } @@ -44,7 +44,7 @@ struct SliderView: View { HStack { label .frame(maxWidth: 150, alignment: .leading) - Slider(value: $currentValue, in: min...max) { editing in + Slider(value: $currentValue, in: min...max, step: 0.01) { editing in if !editing { value = currentValue } @@ -97,10 +97,9 @@ struct ThemedColorSettingCell: View { } } -@MainActor struct DecorationSetting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting var body: some View { Form { @@ -155,5 +154,10 @@ struct DecorationSetting: View { } #Preview("Decoration Setting") { - DecorationSetting() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return DecorationSetting() + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Shared/Setting/Documentation.swift b/Shared/Setting/Documentation.swift index 7db48ee..3ab363d 100644 --- a/Shared/Setting/Documentation.swift +++ b/Shared/Setting/Documentation.swift @@ -69,7 +69,7 @@ struct ParagraphView: View { struct Documentation: View { private let parser = MarkdownParser() @State fileprivate var articles: [Paragraph] = [] - @Environment(\.watchSetting) var watchSetting + @Environment(WatchSetting.self) var watchSetting var body: some View { Form { @@ -116,7 +116,9 @@ struct Documentation: View { } #Preview("Documentation") { - Documentation() + let watchSetting = WatchSetting() + return Documentation() + .environment(watchSetting) #if os(macOS) .frame(width: 500, height: 300) #endif diff --git a/Shared/Setting/LayoutSetting.swift b/Shared/Setting/LayoutSetting.swift index 66c0144..e7bf439 100644 --- a/Shared/Setting/LayoutSetting.swift +++ b/Shared/Setting/LayoutSetting.swift @@ -174,15 +174,14 @@ struct LayoutSettingCell: View { #endif struct LayoutSetting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting #if os(macOS) || os(visionOS) static let nameMapping = [ "space": NSLocalizedString("空格", comment: "Space separator"), "dot": NSLocalizedString("・", comment: "・"), "none": NSLocalizedString("無", comment: "No separator") ] - @Environment(\.chineseCalendar) var chineseCalendar #endif #if os(macOS) @State var fontHandler = FontHandler() @@ -315,5 +314,10 @@ struct LayoutSetting: View { } #Preview("LayoutSetting") { - LayoutSetting() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return LayoutSetting() + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Shared/Setting/Location.swift b/Shared/Setting/Location.swift index 70e7987..45adf94 100644 --- a/Shared/Setting/Location.swift +++ b/Shared/Setting/Location.swift @@ -65,59 +65,59 @@ struct LocationSelection: Equatable { @Observable fileprivate class LocationData { var locationManager: LocationManager? - var watchLayout: WatchLayout? + var calendarConfigure: CalendarConfigure? var locationUnavailable = false var timezoneLongitude: CGFloat { - let logitude = (CGFloat(Calendar.current.timeZone.secondsFromGMT()) - Calendar.current.timeZone.daylightSavingTimeOffset()) / 240 + let timezone = calendarConfigure?.timezone ?? Calendar.current.timeZone + let logitude = (CGFloat(timezone.secondsFromGMT()) - timezone.daylightSavingTimeOffset()) / 240 return ((logitude + 180) %% 360) - 180 } var locationEnabled: Bool { get { - (locationManager?.enabled ?? false) || (watchLayout?.location != nil) + calendarConfigure?.location(locationManager: locationManager) != nil } set { if newValue { - locationManager?.enabled = true - if !gpsEnabled { - watchLayout?.location = CGPoint(x: 0.0, y: timezoneLongitude) - } + calendarConfigure?.customLocation = calendarConfigure?.customLocation ?? CGPoint(x: 0.0, y: timezoneLongitude) } else { - locationManager?.enabled = false - watchLayout?.location = nil + calendarConfigure?.locationEnabled = false + calendarConfigure?.customLocation = nil } } } var gpsEnabled: Bool { get { - locationManager?.enabled ?? false + if let locationManager = locationManager, let calendarConfigure = calendarConfigure { + return locationManager.enabled && calendarConfigure.locationEnabled + } else { + return false + } } set { if newValue { - locationManager?.enabled = true + calendarConfigure?.locationEnabled = true if !gpsEnabled { locationUnavailable = true + } else { + locationManager?.enabled = true } } else { - locationManager?.enabled = false - watchLayout?.location = watchLayout?.location ?? locationManager?.location ?? CGPoint(x: 0.0, y: timezoneLongitude) + calendarConfigure?.locationEnabled = false + calendarConfigure?.customLocation = calendarConfigure?.customLocation ?? locationManager?.location ?? CGPoint(x: 0.0, y: timezoneLongitude) } } } - var gpsLocation: CGPoint? { - locationManager?.location - } - var manualLocation: CGPoint? { - watchLayout?.location + calendarConfigure?.customLocation } var latitudeSelection: LocationSelection { get { LocationSelection.from(value: manualLocation?.x ?? 0) } set { - watchLayout?.location?.x = newValue.value + calendarConfigure?.customLocation?.x = newValue.value } } @@ -125,17 +125,17 @@ struct LocationSelection: Equatable { get { LocationSelection.from(value: manualLocation?.y ?? CGFloat(Calendar.current.timeZone.secondsFromGMT()) / 240) } set { - watchLayout?.location?.y = newValue.value + calendarConfigure?.customLocation?.y = newValue.value } } var location: CGPoint? { - self.gpsLocation ?? self.manualLocation + calendarConfigure?.location(locationManager: locationManager) } - func setup(locationManager: LocationManager, watchLayout: WatchLayout) { + func setup(locationManager: LocationManager, calendarConfigure: CalendarConfigure) { self.locationManager = locationManager - self.watchLayout = watchLayout + self.calendarConfigure = calendarConfigure } } @@ -194,10 +194,10 @@ struct OnSubmitTextField: View { struct Location: View { @State fileprivate var locationData = LocationData() - @Environment(\.watchSetting) var watchSetting - @Environment(\.locationManager) var locationManager - @Environment(\.watchLayout) var watchLayout - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchSetting.self) var watchSetting + @Environment(LocationManager.self) var locationManager + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(ChineseCalendar.self) var chineseCalendar var body: some View { Form { @@ -363,10 +363,10 @@ struct Location: View { } .formStyle(.grouped) .task { - locationData.setup(locationManager: locationManager, watchLayout: watchLayout) + locationData.setup(locationManager: locationManager, calendarConfigure: calendarConfigure) } .onChange(of: locationData.location) { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, location: locationData.location) + chineseCalendar.update(time: watchSetting.effectiveTime, location: locationData.location) } .navigationTitle(Text("經緯度", comment: "Geo Location section")) #if os(iOS) @@ -382,5 +382,14 @@ struct Location: View { } #Preview("Location") { - Location() + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + + return Location() + .environment(chineseCalendar) + .environment(locationManager) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/Shared/Setting/RingSetting.swift b/Shared/Setting/RingSetting.swift index 0eadf3b..c06bba9 100644 --- a/Shared/Setting/RingSetting.swift +++ b/Shared/Setting/RingSetting.swift @@ -350,12 +350,9 @@ struct GradientSliderView: View { } } - - -@MainActor struct RingSetting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting var body: some View { Form { @@ -376,6 +373,14 @@ struct RingSetting: View { GradientSliderView(text: Text("大字", comment: "Day Ring Gradient"), gradient: watchLayout.binding(\.centerFontColor), allowLoop: false) .frame(height: height - loopSize) } + + Section(header: Text("起始角", comment: "Starting Phase")) { + SliderView(value: watchLayout.binding(\.startingPhase.zeroRing), min: -1, max: 1, label: Text("節氣")) + SliderView(value: watchLayout.binding(\.startingPhase.firstRing), min: -1, max: 1, label: Text("年輪")) + SliderView(value: watchLayout.binding(\.startingPhase.secondRing), min: -1, max: 1, label: Text("月輪")) + SliderView(value: watchLayout.binding(\.startingPhase.thirdRing), min: -1, max: 1, label: Text("日輪")) + SliderView(value: watchLayout.binding(\.startingPhase.fourthRing), min: -1, max: 1, label: Text("時輪")) + } } .formStyle(.grouped) .navigationTitle(Text("輪色", comment: "Rings Color Setting")) @@ -392,5 +397,10 @@ struct RingSetting: View { } #Preview("Ring Setting") { - RingSetting() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return RingSetting() + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Shared/Setting/StatusState.swift b/Shared/Setting/StatusState.swift index 8c0914f..2bcdfcc 100644 --- a/Shared/Setting/StatusState.swift +++ b/Shared/Setting/StatusState.swift @@ -14,12 +14,12 @@ class StatusState: Equatable { var statusBar: WatchLayout.StatusBar var calendarSetting: Int - init(locationManager: LocationManager, watchLayout: WatchLayout, watchSetting: WatchSetting) { - location = locationManager.location ?? watchLayout.location + init(locationManager: LocationManager, watchLayout: WatchLayout, calendarConfigure: CalendarConfigure, watchSetting: WatchSetting) { + location = calendarConfigure.location(locationManager: locationManager) date = watchSetting.displayTime - timezone = watchSetting.timezone + timezone = calendarConfigure.timezone statusBar = watchLayout.statusBar - calendarSetting = (watchLayout.globalMonth ? 2 : 0) + (watchLayout.apparentTime ? 1 : 0) + calendarSetting = (calendarConfigure.globalMonth ? 2 : 0) + (calendarConfigure.apparentTime ? 1 : 0) } static func == (lhs: StatusState, rhs: StatusState) -> Bool { diff --git a/Shared/Setting/ThemesList.swift b/Shared/Setting/ThemesList.swift index f665a98..c66f54e 100644 --- a/Shared/Setting/ThemesList.swift +++ b/Shared/Setting/ThemesList.swift @@ -21,7 +21,7 @@ struct TextDocument: FileDocument { return FileWrapper(regularFileWithContents: data) } - static var readableContentTypes: [UTType] = [.text] + static let readableContentTypes: [UTType] = [.text] var text: String = "" init?(_ text: String?) { @@ -31,7 +31,6 @@ struct TextDocument: FileDocument { return nil } } - } private func loadThemes(data: [ThemeData]) -> [String: [ThemeData]] { @@ -51,11 +50,101 @@ private func loadThemes(data: [ThemeData]) -> [String: [ThemeData]] { return newThemes } +func numberedName(_ baseName: String, number: Int) -> String { + if number <= 1 { + return baseName + } else { + return "\(baseName) \(number)" + } +} + +func reverseNumberedName(_ name: String) -> (String, Int) { + let namePattern = /^(.*) (\d+)$/ + if let match = try? namePattern.firstMatch(in: name) { + return (String(match.output.1), Int(match.output.2)!) + } else { + return (name, 1) + } +} + +func read(file: URL) throws -> (name: String, code: String) { + let accessing = file.startAccessingSecurityScopedResource() + defer { + if accessing { + file.stopAccessingSecurityScopedResource() + } + } + let code = try String(contentsOf: file) + let nameComponent = file.lastPathComponent + let namePattern = /^([^\.]+)\.?.*$/ + let name = try namePattern.firstMatch(in: nameComponent)?.output.1 + let actualName = if let name = name { + String(name) + } else { + nameComponent + } + return (name: actualName, code: code) +} + +#if os(macOS) +@MainActor +func writeFile(name: String, code: String) { + 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 = "\(name).txt" + panel.begin { result in + if result == .OK, let file = panel.url { + do { + try 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(handler: @escaping (URL) throws -> ()) { + 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 { + try handler(file) + } catch { + let alert = NSAlert() + alert.messageText = NSLocalizedString("讀不入", comment: "Load Failed") + alert.informativeText = error.localizedDescription + alert.alertStyle = .critical + alert.beginSheetModal(for: panel) + } + } + } +} +#endif + struct ThemesList: View { - @Query private var dataStack: [ThemeData] + @Query(sort: \ThemeData.modifiedDate, order: .reverse) private var dataStack: [ThemeData] @Environment(\.modelContext) private var modelContext - @Environment(\.watchSetting) var watchSetting - @Environment(\.watchLayout) var watchLayout + @Environment(WatchSetting.self) var watchSetting + @Environment(WatchLayout.self) var watchLayout @State private var renameAlert = false @State private var createAlert = false @@ -104,7 +193,7 @@ struct ThemesList: View { } let newThemeConfirm = Button(NSLocalizedString("此名甚善", comment: "Confirm adding Settings"), role: .destructive) { - let newTheme = ThemeData(name: newName, code: WatchLayout.shared.encode()) + let newTheme = ThemeData(name: newName, code: watchLayout.encode()) modelContext.insert(newTheme) do { try modelContext.save() @@ -129,13 +218,7 @@ struct ThemesList: View { let readButton = Button { #if os(macOS) - readFile(context: modelContext) - do { - try modelContext.save() - } catch { - errorMsg = error.localizedDescription - errorAlert = true - } + readFile(handler: handleFile) #elseif os(iOS) || os(visionOS) isImporting = true #endif @@ -157,45 +240,40 @@ struct ThemesList: View { .menuStyle(.automatic) Form { - let deviceNames = themes.keys.sorted(by: {$0 > $1}).sorted(by: {prev, _ in prev == currentDeviceName}) - ForEach(deviceNames, id: \.self) { key in - Section(key) { + if dataStack.count > 0 { + let deviceNames = themes.keys.sorted(by: {$0 > $1}).sorted(by: {prev, _ in prev == currentDeviceName}) + ForEach(deviceNames, id: \.self) { key in + Section(key) { #if os(iOS) - if key == currentDeviceName { - moreMenu - .labelStyle(.titleOnly) - .frame(maxWidth: .infinity) - } + if key == currentDeviceName { + moreMenu + .labelStyle(.titleOnly) + .frame(maxWidth: .infinity) + } #endif - ForEach(themes[key]!, id: \.self) { theme in - if !theme.isNil { - let dateLabel = if Calendar.current.isDate(theme.modifiedDate!, inSameDayAs: .now) { - Text(theme.modifiedDate!, style: .time) - .foregroundStyle(.secondary) - } else { - Text(theme.modifiedDate!, style: .date) - .foregroundStyle(.secondary) - } - - let applyButton = Button { - target = theme - switchAlert = true - } label: { - Label("用", systemImage: "cursorarrow.click.2") - } - - let saveButton = Button { + ForEach(themes[key]!, id: \.self) { theme in + if !theme.isNil { + let dateLabel = if Calendar.current.isDate(theme.modifiedDate!, inSameDayAs: .now) { + Text(theme.modifiedDate!, style: .time) + .foregroundStyle(.secondary) + } else { + Text(theme.modifiedDate!, style: .date) + .foregroundStyle(.secondary) + } + + let saveButton = Button { #if os(macOS) - writeFile(theme: theme) + if !theme.isNil { + writeFile(name: theme.name!, code: theme.code!) + } #elseif os(iOS) || os(visionOS) - target = theme - isExporting = true + target = theme + isExporting = true #endif - } label: { - Label("寫下", systemImage: "square.and.arrow.up") - } - - if theme.name! != AppInfo.defaultName { + } label: { + Label("寫下", systemImage: "square.and.arrow.up") + } + let deleteButton = Button(role: .destructive) { target = theme deleteAlert = true @@ -205,87 +283,66 @@ struct ThemesList: View { let renameButton = Button { target = theme - newName = validName(theme.name!) + newName = validName(theme.name!, device: theme.deviceName!) renameAlert = true } label: { Label("更名", systemImage: "rectangle.and.pencil.and.ellipsis.rtl") } -#if os(macOS) - HStack { - Menu { - applyButton - renameButton - saveButton - deleteButton - } label: { - Text(theme.name!) - } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.accessoryBar) - .labelStyle(.titleAndIcon) - Spacer() - dateLabel + + let nameLabel = if theme.name! != AppInfo.defaultName { + Text(theme.name!) + } else { + Text("常用") } -#else - Menu { - applyButton - renameButton - saveButton - deleteButton + + Button { + if theme.name! != AppInfo.defaultName || theme.deviceName! != AppInfo.deviceName { + target = theme + switchAlert = true + } } label: { HStack { - Text(theme.name!) + nameLabel Spacer() dateLabel } } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.borderless) - .labelStyle(.titleAndIcon) - .tint(.primary) -#endif - } else { #if os(macOS) - HStack { - Menu { - applyButton - saveButton - } label: { - Text("常用") - } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.accessoryBar) - .labelStyle(.titleAndIcon) - Spacer() - dateLabel - } + .buttonStyle(.accessoryBar) #else - Menu { - applyButton + .buttonStyle(.borderless) +#endif + .tint(.primary) + .labelStyle(.titleAndIcon) + .contextMenu { saveButton - } label: { - HStack { - Text("常用") - Spacer() - dateLabel + if theme.name! != AppInfo.defaultName { + renameButton + } + if theme.name! != AppInfo.defaultName || theme.deviceName! != AppInfo.deviceName { + deleteButton } + } preview: { + let themeLayout = { + let layout = WatchLayout() + layout.update(from: theme.code!) + return layout + }() + Icon(watchLayout: themeLayout, preview: true) + .frame(width: 120, height: 120) } - .menuIndicator(.hidden) - .menuStyle(.button) - .buttonStyle(.borderless) - .labelStyle(.titleAndIcon) - .tint(.primary) -#endif } } } } + } else { + Text("混沌初開,萬物未成") } } .formStyle(.grouped) + .onAppear { + removeDuplicates() + } .alert(NSLocalizedString("更名", comment: "Rename action"), isPresented: $renameAlert) { TextField("", text: $newName) .labelsHidden() @@ -313,9 +370,7 @@ struct ThemesList: View { #else watchLayout.update(from: target.code!) #endif -#if os(iOS) - WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) -#elseif os(macOS) +#if os(macOS) if let delegate = AppDelegate.instance { delegate.update() delegate.watchPanel.panelPosition() @@ -367,17 +422,8 @@ struct ThemesList: View { switch result { case .success(let file): do { - let accessing = file.startAccessingSecurityScopedResource() - defer { - if accessing { - file.stopAccessingSecurityScopedResource() - } - } - let themeCode = try String(contentsOf: file) - let name = file.lastPathComponent - let namePattern = /^([^\.]+)\.?.*$/ - let themeName = try namePattern.firstMatch(in: name)?.output.1 - let theme = ThemeData(name: validName(themeName != nil ? String(themeName!) : name), code: themeCode) + let (name, code) = try read(file: file) + let theme = ThemeData(name: validName(name), code: code) modelContext.insert(theme) try modelContext.save() } catch { @@ -416,91 +462,48 @@ struct ThemesList: View { } func validName(_ name: String, device: String? = nil) -> String { - func numberedName(_ baseName: String, number: Int) -> String { - if number <= 1 { - return baseName - } else { - return "\(baseName) \(i)" - } - } - let namePattern = /^(.*) (\d+)$/ - let baseName: String - var i: Int - if let match = try? namePattern.firstMatch(in: name) { - baseName = String(match.output.1) - i = Int(match.output.2)! - } else { - baseName = name - i = 1 - } + var (baseName, i) = reverseNumberedName(name) while !validateName(numberedName(baseName, number: i), onDevice: device ?? currentDeviceName) { i += 1 } return numberedName(baseName, number: 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) + func removeDuplicates() { + var records = Set() + for data in dataStack { + if data.isNil { + modelContext.delete(data) + } else { + if records.contains("deviceName: \(data.deviceName!), themeName: \(data.name!)") { + modelContext.delete(data) + } else { + records.insert("deviceName: \(data.deviceName!), themeName: \(data.name!)") } } } } +#if os(macOS) @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) - } - } - } + func handleFile(_ file: URL) throws { + let themeCode = try String(contentsOf: file) + let name = file.lastPathComponent + let namePattern = /^([^\.]+)\.?.*$/ + let themeName = try namePattern.firstMatch(in: name)?.output.1 + let theme = ThemeData(name: validName(themeName != nil ? String(themeName!) : name), code: themeCode) + modelContext.insert(theme) + try modelContext.save() } #endif } #Preview("Themes") { - ThemesList() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return ThemesList() + .modelContainer(DataSchema.container) + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Shared/Views/HoverView.swift b/Shared/Views/HoverView.swift index 4c52538..06043fe 100644 --- a/Shared/Views/HoverView.swift +++ b/Shared/Views/HoverView.swift @@ -8,8 +8,7 @@ import SwiftUI import Observation -@MainActor -@Observable class EntitySelection { +@Observable final class EntitySelection { @ObservationIgnored var entityNotes = EntityNotes() @ObservationIgnored var timer: Timer? = nil @ObservationIgnored var _activeNote: [EntityNotes.EntityNote] = [] { @@ -55,7 +54,7 @@ private func edgeSafePos(pos: CGPoint, bounds: CGRect, screen: CGSize) -> CGPoin } struct Hover: View { - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout @State var entityPresenting: EntitySelection @Binding var bounds: CGRect @Binding var tapPos: CGPoint? diff --git a/Shared/Views/Icon.swift b/Shared/Views/Icon.swift index 8840e39..e6980f6 100644 --- a/Shared/Views/Icon.swift +++ b/Shared/Views/Icon.swift @@ -11,10 +11,17 @@ struct Icon: View { static let frameOffset: CGFloat = 0.03 @Environment(\.colorScheme) var colorScheme let watchLayout: WatchLayout - let widthScale = 1.5 + let widthScale: CGFloat + let preview: Bool - init(watchLayout: WatchLayout) { + init(watchLayout: WatchLayout, preview: Bool = false) { self.watchLayout = watchLayout + self.preview = preview + if preview { + widthScale = 1.1 + } else { + widthScale = 1.5 + } } var body: some View { @@ -31,12 +38,20 @@ struct Icon: View { let outerBound = RoundedRect(rect: CGRect(origin: .zero, size: size), nodePos: cornerSize, ankorPos: cornerSize * 0.2) let firstRingOuter = outerBound.shrink(by: ZeroRing.width * shortEdge * widthScale) let secondRingOuter = firstRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) - let innerBound = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let thirdRingOuter = secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + let innerBound = if preview { + thirdRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + } else { + secondRingOuter.shrink(by: Ring.paddedWidth * shortEdge * widthScale) + } ZStack { - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: false, ticks: ChineseCalendar.Ticks(), startingAngle: 0, angle: 0.3, textFont: WatchFont(watchLayout.textFont), textColor: clearColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: 1, minorTickAlpha: 1, majorTickColor: clearColor, minorTickColor: clearColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: [], shadowDirection: shadowDirection, entityNotes: nil, shadowSize: watchLayout.shadowSize, highlightType: .flicker, offset: .zero) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: false, ticks: ChineseCalendar.Ticks(), startingAngle: 0, angle: 0.45, textFont: WatchFont(watchLayout.textFont), textColor: clearColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: 1, minorTickAlpha: 1, majorTickColor: clearColor, minorTickColor: clearColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: [], shadowDirection: shadowDirection, entityNotes: nil, shadowSize: watchLayout.shadowSize, highlightType: .flicker, offset: .zero) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: false, ticks: ChineseCalendar.Ticks(), startingAngle: 0, angle: preview ? 0.9 : 0.3, textFont: WatchFont(watchLayout.textFont), textColor: clearColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: 1, minorTickAlpha: 1, majorTickColor: clearColor, minorTickColor: clearColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: [], shadowDirection: shadowDirection, entityNotes: nil, shadowSize: watchLayout.shadowSize, highlightType: .flicker, offset: .zero) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: false, ticks: ChineseCalendar.Ticks(), startingAngle: 0, angle: preview ? 0.8 : 0.45, textFont: WatchFont(watchLayout.textFont), textColor: clearColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: 1, minorTickAlpha: 1, majorTickColor: clearColor, minorTickColor: clearColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: [], shadowDirection: shadowDirection, entityNotes: nil, shadowSize: watchLayout.shadowSize, highlightType: .flicker, offset: .zero) + if preview { + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: false, ticks: ChineseCalendar.Ticks(), startingAngle: 0, angle: 0.7, textFont: WatchFont(watchLayout.textFont), textColor: clearColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: 1, minorTickAlpha: 1, majorTickColor: clearColor, minorTickColor: clearColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: [], shadowDirection: shadowDirection, entityNotes: nil, shadowSize: watchLayout.shadowSize, highlightType: .flicker, offset: .zero) + } Core(viewSize: size, dateString: "", timeString: "", font: WatchFont(watchLayout.centerFont), maxLength: 5, textColor: watchLayout.centerFontColor, outerBound: innerBound, innerColor: coreColor, backColor: backColor, centerOffset: 0, shadowDirection: shadowDirection, shadowSize: watchLayout.shadowSize) } } @@ -44,7 +59,7 @@ struct Icon: View { } #Preview("Icon") { - let watchLayout = WatchLayout.shared + let watchLayout = WatchLayout() watchLayout.loadStatic() return Icon(watchLayout: watchLayout) .frame(width: 120, height: 120) diff --git a/Shared/Views/SwiftUIUtilities.swift b/Shared/Views/SwiftUIUtilities.swift deleted file mode 100644 index 92636f7..0000000 --- a/Shared/Views/SwiftUIUtilities.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SwiftUIUtilities.swift -// Chinendar -// -// Created by Leo Liu on 5/11/23. -// - -import SwiftUI - -struct StartingPhase { - var zeroRing: CGFloat = 0.0 - var firstRing: CGFloat = 0.0 - var secondRing: CGFloat = 0.0 - var thirdRing: CGFloat = 0.0 - var fourthRing: CGFloat = 0.0 -} - -func applyGradient(gradient: WatchLayout.Gradient, startingAngle: CGFloat) -> Gradient { - let colors: [CGColor] - let locations: [CGFloat] - if startingAngle >= 0 { - colors = gradient.colors.reversed() - locations = gradient.locations.map { 1 - $0 }.reversed() - } else { - colors = gradient.colors - locations = gradient.locations - } - return Gradient(stops: zip(colors, locations).map { Gradient.Stop(color: Color(cgColor: $0.0), location: $0.1) }) -} diff --git a/Shared/Views/WatchFaceBasics.swift b/Shared/Views/WatchFaceBasics.swift index bc65d9c..001287e 100644 --- a/Shared/Views/WatchFaceBasics.swift +++ b/Shared/Views/WatchFaceBasics.swift @@ -195,9 +195,15 @@ struct Ring: View { self.pathWithAngle = anglePath self.startingAngle = Angle(radians: realStartingAngle) self.highlightAngle = Angle(radians: realAngle) - self.highlightGradient = Gradient(stops: [.init(color: Color(white: 1, opacity: 0.5), location: 0), - .init(color: .clear, location: min(angle, width / 4 * shortEdge / realLength)), - .init(color: .clear, location: 1)]) + self.highlightGradient = if startingAngle >= 0 { + Gradient(stops: [.init(color: Color(white: 1, opacity: 0.5), location: 0), + .init(color: .clear, location: min(angle, width / 4 * shortEdge / realLength)), + .init(color: .clear, location: 1)]) + } else { + Gradient(stops: [.init(color: .clear, location: 0), + .init(color: .clear, location: 1 - min(angle, width / 4 * shortEdge / realLength)), + .init(color: Color(white: 1, opacity: 0.5), location: 1)]) + } self.highlightType = highlightType var drawableMarks = [DrawableMark]() diff --git a/Shared/Views/WatchFaceView.swift b/Shared/Views/WatchFaceView.swift index 5e70c37..832a972 100644 --- a/Shared/Views/WatchFaceView.swift +++ b/Shared/Views/WatchFaceView.swift @@ -158,7 +158,6 @@ struct Watch: View { let displayZeroRing: Bool let displaySubquarter: Bool let compact: Bool - let phase = StartingPhase() let watchLayout: WatchLayout let markSize: CGFloat let widthScale: CGFloat @@ -227,19 +226,19 @@ struct Watch: View { 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) + ZeroRing(width: ZeroRing.width * widthScale, viewSize: size, compact: compact, textFont: WatchFont(watchLayout.textFont), outerRing: outerBound, startingAngle: watchLayout.startingPhase.zeroRing, oddTicks: chineseCalendar.oddSolarTerms.map { CGFloat($0) }, evenTicks: chineseCalendar.evenSolarTerms.map { CGFloat($0) }, oddColor: oddSTColor, evenColor: evenSTColor, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese, offset: shift) } let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType, offset: shift) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: watchLayout.startingPhase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType, offset: shift) .scaleEffect(1 + directedScale.value * 0.25, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: watchLayout.startingPhase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.65, blendDuration: 0.2), value: directedScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: watchLayout.startingPhase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: thirdRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: watchLayout.startingPhase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: fourthRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType, offset: shift) .scaleEffect(1 + directedScale.value, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.75, blendDuration: 0.2), value: directedScale) let timeString = displaySubquarter ? chineseCalendar.timeString : (chineseCalendar.hourString + chineseCalendar.shortQuarterString) @@ -265,7 +264,6 @@ struct DateWatch: View { let shrink: Bool let displayZeroRing: Bool let compact: Bool - let phase = StartingPhase() let watchLayout: WatchLayout let markSize: CGFloat let widthScale: CGFloat @@ -323,13 +321,13 @@ struct DateWatch: View { 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) + ZeroRing(width: ZeroRing.width * widthScale, viewSize: size, compact: compact, textFont: WatchFont(watchLayout.textFont), outerRing: outerBound, startingAngle: watchLayout.startingPhase.zeroRing, oddTicks: chineseCalendar.oddSolarTerms.map { CGFloat($0) }, evenTicks: chineseCalendar.evenSolarTerms.map { CGFloat($0) }, oddColor: oddSTColor, evenColor: evenSTColor, oddTexts: ChineseCalendar.oddSolarTermChinese, evenTexts: ChineseCalendar.evenSolarTermChinese) } let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: phase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.monthTicks, startingAngle: watchLayout.startingPhase.firstRing, angle: chineseCalendar.currentDayInYear, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.firstRing, outerRing: firstRingOuter, marks: firstRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: phase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.dayTicks, startingAngle: watchLayout.startingPhase.secondRing, angle: chineseCalendar.currentDayInMonth, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.secondRing, outerRing: secondRingOuter, marks: secondRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) @@ -356,7 +354,6 @@ struct TimeWatch: View { let displayZeroRing: Bool let displaySubquarter: Bool let compact: Bool - let phase = StartingPhase() let watchLayout: WatchLayout let markSize: CGFloat let widthScale: CGFloat @@ -414,10 +411,10 @@ struct TimeWatch: View { ZStack { let _ = entityNotes?.reset() - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: phase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.hourTicks, startingAngle: watchLayout.startingPhase.thirdRing, angle: chineseCalendar.currentHourInDay, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: watchLayout.thirdRing, outerRing: firstRingOuter, marks: thirdRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: showsWidgetContainerBackground ? watchLayout.shadowSize : 0.0, highlightType: highlightType) .scaleEffect(1 + directedScale.value * 0.5, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.6, blendDuration: 0.2), value: directedScale) - Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: phase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) + Ring(width: Ring.paddedWidth * widthScale, viewSize: size, compact: compact, ticks: chineseCalendar.subhourTicks, startingAngle: watchLayout.startingPhase.fourthRing, angle: chineseCalendar.subhourInHour, textFont: WatchFont(watchLayout.textFont), textColor: textColor, alpha: watchLayout.shadeAlpha, majorTickAlpha: watchLayout.majorTickAlpha, minorTickAlpha: watchLayout.minorTickAlpha, majorTickColor: majorTickColor, minorTickColor: minorTickColor, backColor: backColor, gradientColor: fourthRingColor, outerRing: secondRingOuter, marks: fourthRingMarks, shadowDirection: shadowDirection, entityNotes: entityNotes, shadowSize: watchLayout.shadowSize, highlightType: highlightType) .scaleEffect(1 + directedScale.value * 0.75, anchor: directedScale.anchor) .animation(.spring(duration: 0.5, bounce: 0.7, blendDuration: 0.2), value: directedScale) diff --git a/Shared/WatchConnectivity.swift b/Shared/WatchConnectivity.swift index 2ba0235..960deda 100644 --- a/Shared/WatchConnectivity.swift +++ b/Shared/WatchConnectivity.swift @@ -6,11 +6,18 @@ // import WatchConnectivity +import Observation +@Observable final class WatchConnectivityManager: NSObject, WCSessionDelegate { - static let shared = WatchConnectivityManager() + @ObservationIgnored let watchLayout: WatchLayout + @ObservationIgnored let calendarConfigure: CalendarConfigure + @ObservationIgnored let locationManager: LocationManager - override private init() { + init(watchLayout: WatchLayout, calendarConfigure: CalendarConfigure, locationManager: LocationManager) { + self.watchLayout = watchLayout + self.calendarConfigure = calendarConfigure + self.locationManager = locationManager super.init() if WCSession.isSupported() { WCSession.default.delegate = self @@ -19,25 +26,35 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { - if let newLayout = message["layout"] as? String { #if os(watchOS) + if let newLayout = message["layout"] as? String { Task(priority: .background) { - let watchLayout = WatchLayout.shared watchLayout.update(from: newLayout) - LocationManager.shared.enabled = watchLayout.locationEnabled - let modelContext = ThemeData.context - watchLayout.saveDefault(context: modelContext) - try? modelContext.save() } -#endif - } else if let request = message["request"] as? String, request == "layout" { -#if os(iOS) - let watchLayout = WatchLayout.shared - if watchLayout.initialized { - sendLayout(watchLayout.encode(includeOffset: false)) + } + if let newConfig = message["config"] as? String { + Task(priority: .background) { + calendarConfigure.update(from: newConfig) + locationManager.enabled = true } -#endif } +#elseif os(iOS) + if let request = message["request"] as? String { + let requests = request.split(separator: /,/, omittingEmptySubsequences: true) + var response = [String: String]() + if requests.contains("layout") { + if watchLayout.initialized { + response["layout"] = watchLayout.encode(includeOffset: false) + } + } + if requests.contains("config") { + if calendarConfigure.initialized { + response["config"] = calendarConfigure.encode(withName: true) + } + } + send(messages: response) + } +#endif } func session(_ session: WCSession, @@ -51,7 +68,7 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { } #endif - func sendLayout(_ message: String) { + func send(messages: [String: String]) { guard WCSession.default.activationState == .activated else { return } #if os(iOS) guard WCSession.default.isWatchAppInstalled else { return } @@ -59,7 +76,7 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { guard WCSession.default.isCompanionAppInstalled else { return } #endif Task(priority: .background) { - WCSession.default.sendMessage(["layout": message], replyHandler: nil) { error in + WCSession.default.sendMessage(messages, replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } } @@ -68,10 +85,11 @@ final class WatchConnectivityManager: NSObject, WCSessionDelegate { #if os(watchOS) func requestLayout() { Task(priority: .background) { - WCSession.default.sendMessage(["request": "layout"], replyHandler: nil) { error in + WCSession.default.sendMessage(["request": "layout,config"], replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } } } #endif } + diff --git a/Vision/Layout.swift b/Vision/Layout.swift index 580a673..f3a824c 100644 --- a/Vision/Layout.swift +++ b/Vision/Layout.swift @@ -9,7 +9,6 @@ import SwiftUI import Observation @Observable final class WatchLayout: MetaWatchLayout { - static let shared = WatchLayout() struct StatusBar: Equatable { @@ -62,12 +61,8 @@ import Observation var statusBar = StatusBar() - 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) + override func encode(includeOffset: Bool = true, includeColor: Bool = true) -> String { + var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor) encoded += "statusBar: \(statusBar.encode())\n" return encoded } @@ -91,24 +86,23 @@ import Observation } @Observable class WatchSetting { - static let shared = WatchSetting() enum Selection: String, CaseIterable { - case datetime, location, ringColor, decoration, markColor, layout, themes + case datetime, location, configs, ringColor, decoration, markColor, layout, themes } enum TabSelection: String, CaseIterable { case spaceTime, design, documentation } var displayTime: Date? = nil - var timezone: TimeZone? = nil var vertical = true var settingIsOpen = false var timeDisplay = "" @ObservationIgnored var previousSelectionSpaceTime: Selection? = nil @ObservationIgnored var previousSelectionDesign: Selection? = nil @ObservationIgnored var previousTabSelection: TabSelection? = nil - - private init() {} + var effectiveTime: Date { + displayTime ?? .now + } func binding(_ keyPath: ReferenceWritableKeyPath) -> Binding { return Binding(get: { self[keyPath: keyPath] }, set: { self[keyPath: keyPath] = $0 }) diff --git a/Vision/Views/Setting.swift b/Vision/Views/Setting.swift index 6c87ea4..86d1327 100644 --- a/Vision/Views/Setting.swift +++ b/Vision/Views/Setting.swift @@ -9,14 +9,14 @@ import SwiftUI import StoreKit struct Setting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting @Environment(\.modelContext) private var modelContext @Environment(\.requestReview) var requestReview @State private var selection: WatchSetting.Selection? @State private var selectedTab: WatchSetting.TabSelection = .spaceTime - let spaceTimePages = [WatchSetting.Selection.datetime, WatchSetting.Selection.location] - let designPages = [WatchSetting.Selection.ringColor, WatchSetting.Selection.decoration, WatchSetting.Selection.markColor, WatchSetting.Selection.layout, WatchSetting.Selection.themes] + let spaceTimePages: [WatchSetting.Selection] = [.datetime, .location, .configs] + let designPages: [WatchSetting.Selection] = [.ringColor, .decoration, .markColor,.layout, .themes] var body: some View { TabView(selection: $selectedTab) { @@ -44,6 +44,10 @@ struct Setting: View { NavigationStack { Location() } + case .configs: + NavigationStack { + ConfigList() + } default: EmptyView() } @@ -116,7 +120,6 @@ struct Setting: View { } .onDisappear { watchSetting.settingIsOpen = false - watchLayout.saveDefault(context: modelContext) if ThemeData.experienced() { requestReview() } @@ -129,6 +132,8 @@ struct Setting: View { Label("日時", systemImage: "clock") case .location: Label("經緯度", systemImage: "location") + case .configs: + Label("日曆墻", systemImage: "globe") case .ringColor: Label("輪色", systemImage: "pencil.and.outline") case .decoration: @@ -145,10 +150,18 @@ struct Setting: View { } #Preview("Settings") { - let watchLayout = WatchLayout.shared - let watchSetting = WatchSetting.shared + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() return Setting() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) .environment(watchLayout) + .environment(calendarConfigure) .environment(watchSetting) } diff --git a/Vision/Views/WatchFace.swift b/Vision/Views/WatchFace.swift index 0f5188f..41c5a8a 100644 --- a/Vision/Views/WatchFace.swift +++ b/Vision/Views/WatchFace.swift @@ -7,11 +7,10 @@ import SwiftUI -@MainActor struct WatchFace: View { - @Environment(\.chineseCalendar) var chineseCalendar - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting @Environment(\.scenePhase) var scenePhase @Environment(\.modelContext) private var modelContext @State var showWelcome = false @@ -102,5 +101,14 @@ struct WatchFace: View { } #Preview("Watch Face") { - WatchFace() + let chineseCalendar = ChineseCalendar() + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + + return WatchFace() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Vision/Views/Welcome.swift b/Vision/Views/Welcome.swift index b94b160..84c2183 100644 --- a/Vision/Views/Welcome.swift +++ b/Vision/Views/Welcome.swift @@ -10,12 +10,12 @@ import SwiftUI struct Welcome: View { let size: CGSize @Environment(\.dismiss) var dismiss - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout var body: some View { let baseLength = min(size.width, size.height) VStack { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: baseLength / 25) { Spacer(minLength: baseLength / 50) .frame(maxHeight: baseLength / 25) @@ -56,6 +56,21 @@ struct Welcome: View { .font(.body) } } + HStack { + Image(systemName: "wand.and.stars") + .font(.largeTitle) + .frame(width: baseLength / 6, height: baseLength / 6) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("新功能", comment: "Welcome, new features - title") + .font(.title3) + .padding(.vertical, baseLength / 100) + .padding(.trailing, baseLength / 25) + .frame(maxWidth: .infinity, alignment: .leading) + Text("新增功能詳情", comment: "Welcome, new features detail") + .font(.body) + } + } } } } @@ -78,5 +93,8 @@ struct Welcome: View { #Preview("Welcome") { - Welcome(size: CGSizeMake(396, 484)) + let watchLayout = WatchLayout() + watchLayout.loadStatic() + return Welcome(size: CGSizeMake(396, 484)) + .environment(watchLayout) } diff --git a/Vision/layout.txt b/Vision/layout.txt index e0ab7b3..e9ca302 100644 --- a/Vision/layout.txt +++ b/Vision/layout.txt @@ -6,6 +6,7 @@ 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 +startingPhase: zeroRing: 0.0; firstRing: 0.0; secondRing: 0.0; thirdRing: 0.0; fourthRing: 0.0 innerColor: 0xFFFFFFFF backColor: 0xFFCCCCCC majorTickColor: 0x00000000 diff --git a/Vision/visionApp.swift b/Vision/visionApp.swift index c9ca2b5..22aab59 100644 --- a/Vision/visionApp.swift +++ b/Vision/visionApp.swift @@ -9,30 +9,35 @@ import SwiftUI @main struct Chinendar: App { - let chineseCalendar = ChineseCalendar(time: .now) - let locationManager = LocationManager.shared - let watchLayout = WatchLayout.shared - let watchSetting = WatchSetting.shared + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() let timer = Timer.publish(every: ChineseCalendar.updateInterval, on: .main, in: .common).autoconnect() @Environment(\.openWindow) var openWindow @Environment(\.dismissWindow) var dismissWindow private var statusState: StatusState { - StatusState(locationManager: locationManager, watchLayout: watchLayout, watchSetting: watchSetting) + StatusState(locationManager: locationManager, watchLayout: watchLayout, calendarConfigure: calendarConfigure, watchSetting: watchSetting) } - init() { - let modelContext = ThemeData.container.mainContext - watchLayout.loadDefault(context: modelContext) - locationManager.requestLocation() + watchLayout.loadDefault(context: DataSchema.container.mainContext) + calendarConfigure.load(name: LocalData.read(context: LocalSchema.container.mainContext)?.configName, context: DataSchema.container.mainContext) + locationManager.enabled = true + watchLayout.autoSave() + calendarConfigure.autoSave() + calendarConfigure.autoSaveName() } var body: some Scene { WindowGroup(id: "WatchFace") { WatchFace() .padding(15) - .environment(\.chineseCalendar, chineseCalendar) - .modelContainer(ThemeData.container) + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(watchLayout) + .environment(watchSetting) .task { self.update() } @@ -76,8 +81,12 @@ struct Chinendar: App { WindowGroup(id: "Settings") { Setting() - .environment(\.chineseCalendar, chineseCalendar) - .modelContainer(ThemeData.container) + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) .onChange(of: statusState) { watchSetting.timeDisplay = String(statusBar(from: chineseCalendar, options: watchLayout).reversed()) } @@ -101,9 +110,12 @@ struct Chinendar: App { } func update() { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, - timezone: watchSetting.timezone ?? Calendar.current.timeZone, - location: locationManager.location ?? watchLayout.location) + chineseCalendar.update(time: watchSetting.effectiveTime, + timezone: calendarConfigure.effectiveTimezone, + location: calendarConfigure.location(locationManager: locationManager), + globalMonth: calendarConfigure.globalMonth, + apparentTime: calendarConfigure.apparentTime, + largeHour: calendarConfigure.largeHour) watchSetting.timeDisplay = String(statusBar(from: chineseCalendar, options: watchLayout).reversed()) } } diff --git a/Watch/Layout.swift b/Watch/Layout.swift index d13247b..9a06422 100644 --- a/Watch/Layout.swift +++ b/Watch/Layout.swift @@ -9,18 +9,13 @@ 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) + override func encode(includeOffset: Bool = true, includeColor: Bool = true) -> String { + var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor) encoded += "dualWatch: \(dualWatch)\n" return encoded } @@ -44,10 +39,10 @@ import Observation } @Observable final class WatchSetting { - static let shared = WatchSetting() var size: CGSize = .zero var displayTime: Date? = nil - - private init() {} + var effectiveTime: Date { + displayTime ?? .now + } } diff --git a/Watch/Views/ContentView.swift b/Watch/Views/ContentView.swift index ca4f334..666c82a 100644 --- a/Watch/Views/ContentView.swift +++ b/Watch/Views/ContentView.swift @@ -9,7 +9,7 @@ import SwiftUI import WidgetKit struct WatchFaceTab: View { - @Environment(\.watchSetting) var watchSetting + @Environment(WatchSetting.self) var watchSetting let proxy: GeometryProxy let tab: Tab @@ -33,9 +33,10 @@ struct WatchFaceTab: View { } struct ContentView: View { - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout @Environment(\.scenePhase) var scenePhase @Environment(\.modelContext) private var modelContext + @Environment(WatchConnectivityManager.self) var watchConnectivityManager var body: some View { GeometryReader { proxy in @@ -57,7 +58,7 @@ struct ContentView: View { .onChange(of: scenePhase) { switch scenePhase { case .active: - WatchConnectivityManager.shared.requestLayout() + watchConnectivityManager.requestLayout() case .inactive, .background: WidgetCenter.shared.reloadAllTimelines() @unknown default: @@ -68,7 +69,20 @@ struct ContentView: View { } #Preview("Watch Face") { - ContentView() - .environment(\.chineseCalendar, .init(time: .now, compact: true)) - .modelContainer(ThemeData.container) + let chineseCalendar = ChineseCalendar(compact: true) + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + let watchConnectivity = WatchConnectivityManager(watchLayout: watchLayout, calendarConfigure: calendarConfigure, locationManager: locationManager) + + return ContentView() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) + .environment(watchConnectivity) } diff --git a/Watch/Views/DateTimeAdjust.swift b/Watch/Views/DateTimeAdjust.swift index b3e95ab..c567106 100644 --- a/Watch/Views/DateTimeAdjust.swift +++ b/Watch/Views/DateTimeAdjust.swift @@ -45,8 +45,8 @@ import Observation } struct DateTimeAdjust: View { - @Environment(\.watchSetting) var watchSetting - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchSetting.self) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar @State private var timeManager = TimeManager() var body: some View { @@ -83,12 +83,16 @@ struct DateTimeAdjust: View { timeManager.setup(watchSetting: watchSetting, chineseCalendar: chineseCalendar) } .onDisappear { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now) + chineseCalendar.update(time: watchSetting.effectiveTime) } } } #Preview("Datetime Adjust") { - DateTimeAdjust() + let chineseCalendar = ChineseCalendar(compact: true) + let watchSetting = WatchSetting() + return DateTimeAdjust() + .environment(chineseCalendar) + .environment(watchSetting) } diff --git a/Watch/Views/Setting.swift b/Watch/Views/Setting.swift index 86b56fc..05fc420 100644 --- a/Watch/Views/Setting.swift +++ b/Watch/Views/Setting.swift @@ -8,20 +8,18 @@ import SwiftUI struct Setting: View { - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout @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) }) } @@ -39,8 +37,15 @@ struct Setting: View { } Section { - NavigationLink(NSLocalizedString("調時", comment: "Change Time")) { + NavigationLink { DateTimeAdjust() + } label: { + Text("調時") + } + NavigationLink { + SwitchConfig() + } label: { + Text("日曆墻") } Toggle(NSLocalizedString("分列日時", comment: "Split Date and Time"), isOn: dualWatch) } footer: { @@ -53,8 +58,20 @@ struct Setting: View { } #Preview("Setting") { - NavigationStack { + let chineseCalendar = ChineseCalendar(compact: true) + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + + return NavigationStack { Setting() - .modelContainer(ThemeData.container) } + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/Watch/Views/SwitchConfig.swift b/Watch/Views/SwitchConfig.swift new file mode 100644 index 0000000..4a29e20 --- /dev/null +++ b/Watch/Views/SwitchConfig.swift @@ -0,0 +1,118 @@ +// +// SwitchConfig.swift +// Chinendar Watch +// +// Created by Leo Liu on 3/30/24. +// + +import SwiftUI +import SwiftData + +struct SwitchConfig: View { + @Query(sort: \ConfigData.modifiedDate, order: .reverse) private var configs: [ConfigData] + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(LocationManager.self) var locationManager + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(\.modelContext) var modelContext + @State private var deleteAlert = false + @State private var errorAlert = false + @State private var errorMsg = "" + @State private var target: ConfigData? = nil + + var body: some View { + List { + if configs.count > 0 { + ForEach(configs, id: \.self) { config in + if !config.isNil { + let chineseDate: String = { + let calConfig = CalendarConfigure(from: config.code!) + let calendar = ChineseCalendar(time: chineseCalendar.time, timezone: calConfig.effectiveTimezone, + location: calConfig.location(locationManager: locationManager), + globalMonth: calConfig.globalMonth, apparentTime: calConfig.apparentTime, + largeHour: calConfig.largeHour) + var displayText = [String]() + displayText.append(calendar.dateString) + let holidays = calendar.holidays + displayText.append(contentsOf: holidays[..: View { @Binding var entityPresenting: EntitySelection @State var tapPos: CGPoint? = nil @@ -56,11 +55,10 @@ struct WatchFace: View { } } -@MainActor struct WatchFaceDate: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar @State var entityPresenting = EntitySelection() var body: some View { @@ -72,11 +70,10 @@ struct WatchFaceDate: View { } } -@MainActor struct WatchFaceTime: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar @State var entityPresenting = EntitySelection() var body: some View { @@ -87,11 +84,10 @@ struct WatchFaceTime: View { } } -@MainActor struct WatchFaceFull: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(WatchSetting.self) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar @State var entityPresenting = EntitySelection() var body: some View { @@ -104,7 +100,10 @@ struct WatchFaceFull: View { } #Preview("Date") { - @Environment(\.watchSetting) var watchSetting + let chineseCalendar = ChineseCalendar(compact: true) + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() return GeometryReader { proxy in WatchFaceDate() @@ -113,10 +112,16 @@ struct WatchFaceFull: View { } } .ignoresSafeArea() + .environment(chineseCalendar) + .environment(watchLayout) + .environment(watchSetting) } #Preview("Time") { - @Environment(\.watchSetting) var watchSetting + let chineseCalendar = ChineseCalendar(compact: true) + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() return GeometryReader { proxy in WatchFaceTime() @@ -125,11 +130,17 @@ struct WatchFaceFull: View { } } .ignoresSafeArea() + .environment(chineseCalendar) + .environment(watchLayout) + .environment(watchSetting) } #Preview("Full") { - @Environment(\.watchSetting) var watchSetting - + let chineseCalendar = ChineseCalendar(compact: true) + let watchLayout = WatchLayout() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + return GeometryReader { proxy in WatchFaceFull() .onAppear{ @@ -137,4 +148,7 @@ struct WatchFaceFull: View { } } .ignoresSafeArea() + .environment(chineseCalendar) + .environment(watchLayout) + .environment(watchSetting) } diff --git a/Watch/layout.txt b/Watch/layout.txt index eaaf840..9cc8f69 100644 --- a/Watch/layout.txt +++ b/Watch/layout.txt @@ -7,6 +7,7 @@ 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 +startingPhase: zeroRing: 0.0; firstRing: 0.0; secondRing: 0.0; thirdRing: 0.0; fourthRing: 0.0 innerColor: 0xFFFFFFFF backColor: 0xFFFFFFFF majorTickColor: 0x00000000 diff --git a/Watch/watchApp.swift b/Watch/watchApp.swift index 9e867e7..9a998d2 100644 --- a/Watch/watchApp.swift +++ b/Watch/watchApp.swift @@ -9,24 +9,34 @@ import SwiftUI @main struct Chinendar: 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 watchConnectivity: WatchConnectivityManager + let chineseCalendar = ChineseCalendar(compact: true) + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() let timer = Timer.publish(every: ChineseCalendar.updateInterval, on: .main, in: .common).autoconnect() init() { - let modelContext = ThemeData.container.mainContext - watchLayout.loadDefault(context: modelContext) - locationManager.requestLocation() + watchConnectivity = .init(watchLayout: watchLayout, calendarConfigure: calendarConfigure, locationManager: locationManager) + watchLayout.loadDefault(context: DataSchema.container.mainContext) + calendarConfigure.load(name: LocalData.read(context: LocalSchema.container.mainContext)?.configName, context: DataSchema.container.mainContext) + locationManager.enabled = true + watchLayout.autoSave() + calendarConfigure.autoSave() + calendarConfigure.autoSaveName() } var body: some Scene { WindowGroup { ContentView() - .modelContainer(ThemeData.container) - .environment(\.chineseCalendar, chineseCalendar) + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) + .environment(watchConnectivity) .task { self.update() await updateCountDownRelevantIntents(chineseCalendar: chineseCalendar.copy) @@ -38,7 +48,11 @@ struct Chinendar: App { } func update() { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, - location: locationManager.location ?? watchLayout.location) + chineseCalendar.update(time: watchSetting.effectiveTime, + timezone: calendarConfigure.effectiveTimezone, + location: calendarConfigure.location(locationManager: locationManager), + globalMonth: calendarConfigure.globalMonth, + apparentTime: calendarConfigure.apparentTime, + largeHour: calendarConfigure.largeHour) } } diff --git a/Widget/Dual.swift b/Widget/Dual.swift index a5b5feb..212abc0 100644 --- a/Widget/Dual.swift +++ b/Widget/Dual.swift @@ -12,18 +12,21 @@ import SwiftUI enum DisplayOrder: String, AppEnum { case dateFirst, timeFirst - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之順序") - static var caseDisplayRepresentations: [DisplayOrder : DisplayRepresentation] = [ + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之順序") + static let caseDisplayRepresentations: [DisplayOrder : DisplayRepresentation] = [ .dateFirst: .init(title: "日左時右"), .timeFirst: .init(title: "時左日右"), ] } -struct MediumConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct MediumConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "MediumIntent" - static var title: LocalizedStringResource = "雙錶" - static var description = IntentDescription("雙錶以同時展現日時,順序可選") + static let title: LocalizedStringResource = "雙錶" + static let description = IntentDescription("雙錶以同時展現日時,順序可選") + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent + @Parameter(title: "順序", default: .dateFirst) var order: DisplayOrder @@ -32,6 +35,7 @@ struct MediumConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMi static var parameterSummary: some ParameterSummary { Summary { + \.$calendarConfig \.$order \.$backAlpha } @@ -41,8 +45,8 @@ struct MediumConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMi struct MediumProvider: ChinendarAppIntentTimelineProvider { typealias Entry = MediumEntry typealias Intent = MediumConfiguration - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func compactCalendar(context: Context) -> Bool { return context.family != .systemExtraLarge @@ -120,6 +124,7 @@ struct MediumWidget: Widget { #Preview("Medium", as: .systemMedium, using: { let intent = MediumProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.order = .dateFirst intent.backAlpha = 0.2 return intent diff --git a/Widget/Full.swift b/Widget/Full.swift index f36e7de..8e04046 100644 --- a/Widget/Full.swift +++ b/Widget/Full.swift @@ -9,16 +9,20 @@ import AppIntents import SwiftUI @preconcurrency import WidgetKit -struct LargeConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct LargeConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "LargeIntent" - static var title: LocalizedStringResource = "全錶" - static var description = IntentDescription("完整錶面") + static let title: LocalizedStringResource = "全錶" + static let description = IntentDescription("完整錶面") + + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent @Parameter(title: "背景灰度", default: 0, controlStyle: .slider, inclusiveRange: (0, 1)) var backAlpha: Double static var parameterSummary: some ParameterSummary { Summary { + \.$calendarConfig \.$backAlpha } } @@ -27,8 +31,8 @@ struct LargeConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMig struct LargeProvider: ChinendarAppIntentTimelineProvider { typealias Entry = LargeEntry typealias Intent = LargeConfiguration - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func compactCalendar(context: Context) -> Bool { return context.family != .systemLarge @@ -92,6 +96,7 @@ struct LargeWidget: Widget { #Preview("Large", as: .systemLarge, using: { let intent = LargeProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.backAlpha = 0.2 return intent }(), widget: { diff --git a/Widget/Protocols.swift b/Widget/Protocols.swift index 2f9f250..f4d56b4 100644 --- a/Widget/Protocols.swift +++ b/Widget/Protocols.swift @@ -19,33 +19,37 @@ protocol ChinendarAppIntentTimelineProvider: AppIntentTimelineProvider where Ent extension ChinendarAppIntentTimelineProvider { func placeholder(in context: Context) -> Entry { - let watchLayout = WatchLayout.shared + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() watchLayout.loadStatic() - let chineseCalendar = ChineseCalendar(time: .now, compact: compactCalendar(context: context)) + let chineseCalendar = ChineseCalendar(timezone: calendarConfigure.effectiveTimezone, location: calendarConfigure.location(locationManager: nil), compact: compactCalendar(context: context), globalMonth: calendarConfigure.globalMonth, apparentTime: calendarConfigure.apparentTime, largeHour: calendarConfigure.largeHour) return Entry(configuration: Entry.Intent(), chineseCalendar: chineseCalendar, watchLayout: watchLayout) } func snapshot(for configuration: Entry.Intent, in context: Context) async -> Entry { - let watchLayout = WatchLayout.shared + let watchLayout = WatchLayout() watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: compactCalendar(context: context)) + let calendarConfigure = CalendarConfigure() + calendarConfigure.load(name: configuration.calendarConfig.name, context: modelContext) + let chineseCalendar = ChineseCalendar(timezone: calendarConfigure.effectiveTimezone, location: calendarConfigure.location(locationManager: locationManager), compact: compactCalendar(context: context), globalMonth: calendarConfigure.globalMonth, apparentTime: calendarConfigure.apparentTime, largeHour: calendarConfigure.largeHour) let entry = Entry(configuration: configuration, chineseCalendar: chineseCalendar, watchLayout: watchLayout) return entry } func timeline(for configuration: Entry.Intent, in context: Context) async -> Timeline { - let watchLayout = WatchLayout.shared + let watchLayout = WatchLayout() watchLayout.loadDefault(context: modelContext, local: true) - let location = await locationManager.getLocation() + let calendarConfigure = CalendarConfigure() + calendarConfigure.load(name: configuration.calendarConfig.name, context: modelContext) + let _ = await locationManager.getLocation() - let chineseCalendar = ChineseCalendar(location: location, compact: compactCalendar(context: context)) + let chineseCalendar = ChineseCalendar(timezone: calendarConfigure.effectiveTimezone, location: calendarConfigure.location(locationManager: locationManager), compact: compactCalendar(context: context), globalMonth: calendarConfigure.globalMonth, apparentTime: calendarConfigure.apparentTime, largeHour: calendarConfigure.largeHour) let originalChineseCalendar = chineseCalendar.copy let entryDates = nextEntryDates(chineseCalendar: chineseCalendar, config: configuration, context: context) var chineseCalendars = [chineseCalendar.copy] for entryDate in entryDates { - chineseCalendar.update(time: entryDate, location: location) + chineseCalendar.update(time: entryDate, location: calendarConfigure.location(locationManager: locationManager)) chineseCalendars.append(chineseCalendar.copy) } let entries: [Entry] = await generateEntries(chineseCalendars: chineseCalendars, watchLayout: watchLayout, configuration: configuration) diff --git a/Widget/Single.swift b/Widget/Single.swift index 089ec61..2b425b7 100644 --- a/Widget/Single.swift +++ b/Widget/Single.swift @@ -12,18 +12,21 @@ import SwiftUI enum DisplayMode: String, AppEnum { case date, time - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之擇一") - static var caseDisplayRepresentations: [DisplayMode : DisplayRepresentation] = [ + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "日時之擇一") + static let caseDisplayRepresentations: [DisplayMode : DisplayRepresentation] = [ .date: .init(title: "日"), .time: .init(title: "時"), ] } -struct SmallConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct SmallConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "SmallIntent" - static var title: LocalizedStringResource = "簡錶" - static var description = IntentDescription("簡化之錶以展現日時之一") + static let title: LocalizedStringResource = "簡錶" + static let description = IntentDescription("簡化之錶以展現日時之一") + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent + @Parameter(title: "型制", default: .time) var mode: DisplayMode @@ -32,6 +35,7 @@ struct SmallConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMig static var parameterSummary: some ParameterSummary { Summary { + \.$calendarConfig \.$mode \.$backAlpha } @@ -41,8 +45,8 @@ struct SmallConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMig struct SmallProvider: ChinendarAppIntentTimelineProvider { typealias Entry = SmallEntry typealias Intent = SmallConfiguration - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func nextEntryDates(chineseCalendar: ChineseCalendar, config: SmallConfiguration, context: Context) -> [Date] { return switch config.mode { @@ -113,6 +117,7 @@ struct SmallWidget: Widget { #Preview("Small Date", as: .systemSmall, using: { let intent = SmallProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.mode = .date intent.backAlpha = 0.2 return intent @@ -124,6 +129,7 @@ struct SmallWidget: Widget { #Preview("Small Time", as: .systemSmall, using: { let intent = SmallProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.mode = .time intent.backAlpha = 0.2 return intent diff --git a/Widget/TaskGroup.swift b/Widget/TaskGroup.swift index 586c43e..7fb8a46 100644 --- a/Widget/TaskGroup.swift +++ b/Widget/TaskGroup.swift @@ -7,9 +7,58 @@ import WidgetKit import AppIntents +import SwiftData + +struct ConfigIntent: AppEntity { + let id: String + var name: String { id } + + static let typeDisplayRepresentation: TypeDisplayRepresentation = "選日曆" + static var defaultQuery = ConfigQuery() + + var displayRepresentation: DisplayRepresentation { + if name != AppInfo.defaultName { + DisplayRepresentation(title: "\(name)") + } else { + DisplayRepresentation("常用") + } + } +} + +struct ConfigQuery: EntityQuery { + func entities(for identifiers: [String]) async throws -> [ConfigIntent] { + try await suggestedEntities().filter { identifiers.contains($0.name) } + } + + func suggestedEntities() async throws -> [ConfigIntent] { + var allConfigs = [ConfigIntent]() + let context = DataSchema.context + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.modifiedDate, order: .reverse)]) + let configs = try context.fetch(descriptor) + for config in configs { + if !config.isNil { + allConfigs.append(ConfigIntent(id: config.name!)) + } + } + if allConfigs.count > 0 { + return allConfigs + } else { + return [ConfigIntent(id: AppInfo.defaultName)] + } + } + + func defaultResult() async -> ConfigIntent? { + let name = LocalData.read(context: LocalSchema.context)?.configName ?? AppInfo.defaultName + return ConfigIntent(id: name) + } +} + +protocol ChinendarWidgetConfigIntent: AppIntent, WidgetConfigurationIntent { + var calendarConfig: ConfigIntent { get set } +} protocol ChinendarEntry: Sendable { - associatedtype Intent: WidgetConfigurationIntent + associatedtype Intent: ChinendarWidgetConfigIntent init(configuration: Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) } diff --git a/Widget/WatchWidgets/Card.swift b/Widget/WatchWidgets/Card.swift index f6090ed..16bbb34 100644 --- a/Widget/WatchWidgets/Card.swift +++ b/Widget/WatchWidgets/Card.swift @@ -9,17 +9,26 @@ import AppIntents import SwiftUI @preconcurrency import WidgetKit -struct CardConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct CardConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "TextCardIntent" - static var title: LocalizedStringResource = "文字片" - static var description = IntentDescription("華曆文字片") + static let title: LocalizedStringResource = "文字片" + static let description = IntentDescription("華曆文字片") + + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent + + static var parameterSummary: some ParameterSummary { + Summary { + \.$calendarConfig + } + } } struct CardProvider: ChinendarAppIntentTimelineProvider { typealias Intent = CardConfiguration typealias Entry = CardEntry - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func nextEntryDates(chineseCalendar: ChineseCalendar, config: CardConfiguration, context: Context) -> [Date] { return chineseCalendar.nextQuarters(count: 12) @@ -51,7 +60,7 @@ struct CardEntryView: View { var body: some View { let chineseCalendar = entry.chineseCalendar - CalendarBadge(dateString: chineseCalendar.dateString, timeString: chineseCalendar.hourString + chineseCalendar.shortQuarterString, color: applyGradient(gradient: entry.watchLayout.centerFontColor, startingAngle: 0), backGround: Color(cgColor: entry.watchLayout.innerColor)) + CalendarBadge(dateString: chineseCalendar.dateString, timeString: chineseCalendar.hourString + chineseCalendar.shortQuarterString, color: applyGradient(gradient: entry.watchLayout.centerFontColor, startingAngle: 0), backGround: Color(cgColor: entry.watchLayout.innerColor), centerFont: entry.watchLayout.centerFont) .containerBackground(Color(cgColor: entry.watchLayout.innerColor), for: .widget) } } @@ -71,7 +80,11 @@ struct DateCardWidget: Widget { } } -#Preview("Card", as: .accessoryRectangular, using: CardProvider.Intent()) { +#Preview("Card", as: .accessoryRectangular, using: { + let intent = CardProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) + return intent +}()) { DateCardWidget() } timelineProvider: { CardProvider() diff --git a/Widget/WatchWidgets/Circular.swift b/Widget/WatchWidgets/Circular.swift index 12bb720..fe79148 100644 --- a/Widget/WatchWidgets/Circular.swift +++ b/Widget/WatchWidgets/Circular.swift @@ -12,23 +12,27 @@ import SwiftUI enum CircularMode: String, AppEnum { case daylight, monthDay - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "圓輪掛件選項") - static var caseDisplayRepresentations: [CircularMode : DisplayRepresentation] = [ + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "圓輪掛件選項") + static let caseDisplayRepresentations: [CircularMode : DisplayRepresentation] = [ .daylight: .init(title: "日月光華"), .monthDay: .init(title: "歲月之輪"), ] } -struct CircularConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct CircularConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "CircularIntent" - static var title: LocalizedStringResource = "圓輪" - static var description = IntentDescription("簡化之輪以展現日時") + static let title: LocalizedStringResource = "圓輪" + static let description = IntentDescription("簡化之輪以展現日時") + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent + @Parameter(title: "型制", default: .daylight) var mode: CircularMode static var parameterSummary: some ParameterSummary { Summary { + \.$calendarConfig \.$mode } } @@ -37,8 +41,8 @@ struct CircularConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntent struct CircularProvider: ChinendarAppIntentTimelineProvider { typealias Entry = CircularEntry typealias Intent = CircularConfiguration - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func nextEntryDates(chineseCalendar: ChineseCalendar, config: CircularConfiguration, context: Context) -> [Date] { return switch config.mode { @@ -126,23 +130,25 @@ struct CircularEntry: TimelineEntry, ChinendarEntry { let innerDirection: CGFloat? let currentColor: Color? let relevance: TimelineEntryRelevance? + let phase: (CGFloat, CGFloat) init(configuration: CircularProvider.Intent, chineseCalendar: ChineseCalendar, watchLayout: WatchLayout) { date = chineseCalendar.time self.configuration = configuration self.chineseCalendar = chineseCalendar self.watchLayout = watchLayout - let phase = StartingPhase() + let phase = watchLayout.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) + outer = (start: 0, end: chineseCalendar.currentDayInYear) + inner = (start: 0, end: chineseCalendar.currentDayInMonth) + outerGradient = applyGradient(gradient: watchLayout.firstRing, startingAngle: 0) + innerGradient = applyGradient(gradient: watchLayout.secondRing, startingAngle: 0) current = nil innerDirection = nil currentColor = nil + self.phase = (phase.firstRing, phase.secondRing) relevance = TimelineEntryRelevance(score: 5, duration: 3600) case .daylight: @@ -151,10 +157,11 @@ struct CircularEntry: TimelineEntry, ChinendarEntry { 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) + outerGradient = applyGradient(gradient: watchLayout.thirdRing, startingAngle: 0) + innerGradient = applyGradient(gradient: watchLayout.secondRing, startingAngle: 0) current = chineseCalendar.currentHourInDay currentColor = Color(cgColor: watchLayout.thirdRing.interpolate(at: chineseCalendar.currentHourInDay)) + self.phase = (phase.thirdRing, phase.thirdRing) relevance = TimelineEntryRelevance(score: 5, duration: 864) } } @@ -166,13 +173,13 @@ struct CircularEntryView: View { var body: some View { switch entry.configuration.mode { case .monthDay: - Circular(outer: entry.outer, inner: entry.inner, outerGradient: entry.outerGradient, innerGradient: entry.innerGradient) + Circular(outer: entry.outer, inner: entry.inner, startingPhase: entry.phase, 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) + Circular(outer: entry.outer, inner: entry.inner, current: entry.current, startingPhase: entry.phase, 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())) @@ -201,6 +208,7 @@ struct CircularWidget: Widget { #Preview("Circular Daylight", as: .accessoryCircular, using: { let intent = CircularProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.mode = .daylight return intent }()) { @@ -211,6 +219,7 @@ struct CircularWidget: Widget { #Preview("Circular Monthday", as: .accessoryCircular, using: { let intent = CircularProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.mode = .monthDay return intent }()) { diff --git a/Widget/WatchWidgets/Corner.swift b/Widget/WatchWidgets/Corner.swift index 6abc02b..8702038 100644 --- a/Widget/WatchWidgets/Corner.swift +++ b/Widget/WatchWidgets/Corner.swift @@ -24,6 +24,7 @@ struct CurveWidget: Widget { #Preview("Sunrise", as: .accessoryCorner, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .sunriseSet return intent }(), widget: { @@ -34,6 +35,7 @@ struct CurveWidget: Widget { #Preview("Moonrise", as: .accessoryCorner, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .moonriseSet return intent }(), widget: { @@ -44,6 +46,7 @@ struct CurveWidget: Widget { #Preview("Solar Terms", as: .accessoryCorner, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .solarTerms return intent }(), widget: { @@ -54,6 +57,7 @@ struct CurveWidget: Widget { #Preview("Moon Phases", as: .accessoryCorner, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .lunarPhases return intent }(), widget: { diff --git a/Widget/WatchWidgets/CountDown.swift b/Widget/WatchWidgets/CountDown.swift index 7556178..624a393 100644 --- a/Widget/WatchWidgets/CountDown.swift +++ b/Widget/WatchWidgets/CountDown.swift @@ -12,8 +12,8 @@ import SwiftUI struct CountDownProvider: ChinendarAppIntentTimelineProvider { typealias Entry = CountDownEntry typealias Intent = CountDownConfiguration - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func nextEntryDates(chineseCalendar: ChineseCalendar, config: CountDownConfiguration, context: Context) -> [Date] { let allTimes = switch config.target { @@ -299,6 +299,7 @@ struct RectWidget: Widget { #Preview("Lunar Phase", as: .accessoryRectangular, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .lunarPhases return intent }()) { @@ -309,6 +310,7 @@ struct RectWidget: Widget { #Preview("Solar Term", as: .accessoryRectangular, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .solarTerms return intent }()) { @@ -319,6 +321,7 @@ struct RectWidget: Widget { #Preview("Sunrise", as: .accessoryRectangular, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .sunriseSet return intent }()) { @@ -329,6 +332,7 @@ struct RectWidget: Widget { #Preview("Moonrise", as: .accessoryRectangular, using: { let intent = CountDownProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) intent.target = .moonriseSet return intent }()) { diff --git a/Widget/WatchWidgets/Relevance.swift b/Widget/WatchWidgets/Relevance.swift index cdcd1b4..67c9357 100644 --- a/Widget/WatchWidgets/Relevance.swift +++ b/Widget/WatchWidgets/Relevance.swift @@ -10,8 +10,8 @@ import AppIntents enum EventType: String, AppEnum { case solarTerms, lunarPhases, sunriseSet, moonriseSet - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "時計掛件選項") - static var caseDisplayRepresentations: [EventType : DisplayRepresentation] = [ + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "時計掛件選項") + static let caseDisplayRepresentations: [EventType : DisplayRepresentation] = [ .solarTerms: .init(title: "節氣"), .lunarPhases: .init(title: "月相"), .sunriseSet: .init(title: "日躔"), @@ -19,16 +19,19 @@ enum EventType: String, AppEnum { ] } -struct CountDownConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct CountDownConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "CurveIntent" - static var title: LocalizedStringResource = "時計" - static var description = IntentDescription("距離次事件之倒計時") + static let title: LocalizedStringResource = "時計" + static let description = IntentDescription("距離次事件之倒計時") + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent @Parameter(title: "目的", default: .solarTerms) var target: EventType static var parameterSummary: some ParameterSummary { Summary { + \.$calendarConfig \.$target } } diff --git a/Widget/WatchWidgets/TextDesp.swift b/Widget/WatchWidgets/TextDesp.swift index 62ccd86..7cd1b1c 100644 --- a/Widget/WatchWidgets/TextDesp.swift +++ b/Widget/WatchWidgets/TextDesp.swift @@ -10,9 +10,9 @@ import SwiftUI @preconcurrency import WidgetKit enum TextWidgetSeparator: String, AppEnum { - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") case space = " ", dot = "・", none = "" - static var caseDisplayRepresentations: [TextWidgetSeparator : DisplayRepresentation] = [ + static let caseDisplayRepresentations: [TextWidgetSeparator : DisplayRepresentation] = [ .none: .init(title: "無"), .dot: .init(title: "・"), .space: .init(title: "空格"), @@ -20,20 +20,22 @@ enum TextWidgetSeparator: String, AppEnum { } enum TextWidgetTime: String, AppEnum { - static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") + static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "讀號選項") case none, hour, hourAndQuarter - static var caseDisplayRepresentations: [TextWidgetTime : DisplayRepresentation] = [ + static let caseDisplayRepresentations: [TextWidgetTime : DisplayRepresentation] = [ .none: .init(title: "無"), .hour: .init(title: "僅時"), .hourAndQuarter: .init(title: "時刻"), ] } -struct TextConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { +struct TextConfiguration: ChinendarWidgetConfigIntent, CustomIntentMigratedAppIntent { static let intentClassName = "SingleLineIntent" - static var title: LocalizedStringResource = "文字" - static var description = IntentDescription("簡單華曆文字") + static let title: LocalizedStringResource = "文字" + static let description = IntentDescription("簡單華曆文字") + @Parameter(title: "選日曆") + var calendarConfig: ConfigIntent @Parameter(title: "日", default: true) var date: Bool @Parameter(title: "時", default: .hour) @@ -42,13 +44,23 @@ struct TextConfiguration: AppIntent, WidgetConfigurationIntent, CustomIntentMigr var holidays: Int @Parameter(title: "讀號", default: .dot) var separator: TextWidgetSeparator + + static var parameterSummary: some ParameterSummary { + Summary { + \.$calendarConfig + \.$date + \.$time + \.$holidays + \.$separator + } + } } struct TextProvider: ChinendarAppIntentTimelineProvider { typealias Intent = TextConfiguration typealias Entry = TextEntry - let modelContext = ThemeData.context - let locationManager = LocationManager.shared + let modelContext = DataSchema.context + let locationManager = LocationManager() func nextEntryDates(chineseCalendar: ChineseCalendar, config: TextConfiguration, context: Context) -> [Date] { switch config.time { @@ -119,7 +131,11 @@ struct LineWidget: Widget { } } -#Preview("Inline", as: .accessoryInline, using: TextProvider.Intent()) { +#Preview("Inline", as: .accessoryInline, using: { + let intent = TextProvider.Intent() + intent.calendarConfig = .init(id: AppInfo.defaultName) + return intent +}()) { LineWidget() } timelineProvider: { TextProvider() diff --git a/Widget/WatchWidgets/WatchWidgetBasic.swift b/Widget/WatchWidgets/WatchWidgetBasic.swift index 325bf14..769253f 100644 --- a/Widget/WatchWidgets/WatchWidgetBasic.swift +++ b/Widget/WatchWidgets/WatchWidgetBasic.swift @@ -74,6 +74,7 @@ struct Circular: View { var outer: (start: CGFloat, end: CGFloat) var inner: (start: CGFloat, end: CGFloat) var current: CGFloat? + var startingPhase: (CGFloat, CGFloat) var innerDirection: CGFloat? var outerGradient: Gradient var innerGradient: Gradient @@ -86,24 +87,30 @@ struct Circular: View { GeometryReader { proxy in let size = proxy.size ZStack { - AngularGradient(gradient: fullColor ? outerGradient : whiteGradient - , center: .center, angle: .degrees(90)).mask { + 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 { + .widgetAccentable() + .scaleEffect(x: startingPhase.0 >= 0 ? 1 : -1) + .rotationEffect(.radians(startingPhase.0 * CGFloat.pi * 2.0)) + 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() + .widgetAccentable() + .scaleEffect(x: startingPhase.1 >= 0 ? 1 : -1) + .rotationEffect(.radians(startingPhase.1 * CGFloat.pi * 2.0)) 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)) + .rotationEffect(.radians((startingPhase.0 + current - 0.25) * CGFloat.pi * 2.0)) + .scaleEffect(x: startingPhase.0 >= 0 ? -1 : 1) .shadow(color: .black, radius: min(size.width, size.height) * 0.05) } } @@ -237,11 +244,12 @@ struct CalendarBadge: View { let timeString: String let color: Gradient let backGround: Color + let centerFont: UIFont @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) + let centerFont = centerFont.withSize(size) attrStr.addAttributes([.font: centerFont, .foregroundColor: CGColor(gray: 1, alpha: 1)], range: NSMakeRange(0, attrStr.length)) return Text(AttributedString(attrStr)) } diff --git a/iOS/Layout.swift b/iOS/Layout.swift index b71f01b..e58e525 100644 --- a/iOS/Layout.swift +++ b/iOS/Layout.swift @@ -7,15 +7,31 @@ import SwiftUI import Observation +import SwiftData @Observable final class WatchLayout: MetaWatchLayout { - static let shared = WatchLayout() + @ObservationIgnored var watchConnectivity: WatchConnectivityManager? = nil var textFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .regular) var centerFont = UIFont(name: "SourceHanSansKR-Heavy", size: UIFont.systemFontSize)! - private override init() { - super.init() + func sendToWatch() { + self.watchConnectivity?.send(messages: [ + "layout": self.encode(includeOffset: false) + ]) + } + + override func autoSave() { + withObservationTracking { + _ = self.encode() + } onChange: { + Task { @MainActor in + let context = DataSchema.container.mainContext + self.saveDefault(context: context) + self.sendToWatch() + self.autoSave() + } + } } var monochrome: Self { @@ -30,12 +46,11 @@ import Observation } @Observable class WatchSetting { - static let shared = WatchSetting() var displayTime: Date? = nil - var timezone: TimeZone? = nil var presentSetting = false var vertical = true - - private init() {} + var effectiveTime: Date { + displayTime ?? .now + } } diff --git a/iOS/Views/Setting.swift b/iOS/Views/Setting.swift index eca5d5e..e0507c4 100644 --- a/iOS/Views/Setting.swift +++ b/iOS/Views/Setting.swift @@ -8,10 +8,10 @@ import SwiftUI struct Setting: View { - @State var locationManager = LocationManager.shared - @Environment(\.chineseCalendar) var chineseCalendar - @Environment(\.watchSetting) var watchSetting - @Environment(\.watchLayout) var watchLayout + @Environment(LocationManager.self) var locationManager + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(WatchSetting.self) var watchSetting + @Environment(CalendarConfigure.self) var calendarConfigure var body: some View { List { @@ -19,41 +19,17 @@ struct Setting: View { 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) - } + Label("日時", systemImage: "clock") } - 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) - } - } + Label("經緯度", systemImage: "location") + } + NavigationLink { + ConfigList() + } label: { + Label("日曆墻", systemImage: "globe") } } @@ -104,7 +80,20 @@ struct Setting: View { } #Preview("Settings") { - NavigationStack { + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + + return NavigationStack { Setting() } + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/iOS/Views/WatchFace.swift b/iOS/Views/WatchFace.swift index 00794bd..da93ce7 100644 --- a/iOS/Views/WatchFace.swift +++ b/iOS/Views/WatchFace.swift @@ -9,11 +9,11 @@ import SwiftUI import WidgetKit import StoreKit -@MainActor struct WatchFace: View { - @Environment(\.chineseCalendar) var chineseCalendar - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(WatchSetting.self) var watchSetting @Environment(\.scenePhase) var scenePhase @Environment(\.modelContext) private var modelContext @Environment(\.requestReview) var requestReview @@ -25,14 +25,10 @@ struct WatchFace: View { @State var timer: Timer? @GestureState private var dragging = false - var presentSetting: Binding { + @MainActor 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)) - if ThemeData.experienced() { requestReview() } @@ -141,10 +137,7 @@ struct WatchFace: View { .onChange(of: scenePhase) { switch scenePhase { case .inactive, .background: - WatchConnectivityManager.shared.sendLayout(watchLayout.encode(includeOffset: false)) WidgetCenter.shared.reloadAllTimelines() - watchLayout.saveDefault(context: modelContext) - try? modelContext.save() default: break } @@ -153,5 +146,18 @@ struct WatchFace: View { } #Preview("Watch Face") { - WatchFace() + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + + return WatchFace() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/iOS/Views/Welcome.swift b/iOS/Views/Welcome.swift index 1527ae5..9f161e1 100644 --- a/iOS/Views/Welcome.swift +++ b/iOS/Views/Welcome.swift @@ -9,11 +9,11 @@ import SwiftUI struct Welcome: View { @Environment(\.dismiss) var dismiss - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout var body: some View { VStack { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 20) { Spacer(minLength: 10) .frame(maxHeight: 20) @@ -54,6 +54,21 @@ struct Welcome: View { .font(.subheadline) } } + HStack { + Image(systemName: "wand.and.stars") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("新功能", comment: "Welcome, new features - title") + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 20) + .frame(maxWidth: .infinity, alignment: .leading) + Text("新增功能詳情", comment: "Welcome, new features detail") + .font(.subheadline) + } + } } } } @@ -76,5 +91,8 @@ struct Welcome: View { #Preview("Welcome") { - Welcome() + let watchLayout = WatchLayout() + watchLayout.loadStatic() + return Welcome() + .environment(watchLayout) } diff --git a/iOS/iOSApp.swift b/iOS/iOSApp.swift index db78886..2e4e978 100644 --- a/iOS/iOSApp.swift +++ b/iOS/iOSApp.swift @@ -9,24 +9,35 @@ import SwiftUI @main struct Chinendar: App { - let watchConnectivity = WatchConnectivityManager.shared - let chineseCalendar = ChineseCalendar(time: .now) - let locationManager = LocationManager.shared - let watchLayout = WatchLayout.shared - let watchSetting = WatchSetting.shared + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + let watchConnectivity: WatchConnectivityManager let timer = Timer.publish(every: ChineseCalendar.updateInterval, on: .main, in: .common).autoconnect() init() { - let modelContext = ThemeData.container.mainContext - watchLayout.loadDefault(context: modelContext) - locationManager.requestLocation() + watchConnectivity = .init(watchLayout: watchLayout, calendarConfigure: calendarConfigure, locationManager: locationManager) + watchLayout.watchConnectivity = watchConnectivity + calendarConfigure.watchConnectivity = watchConnectivity + watchLayout.loadDefault(context: DataSchema.container.mainContext) + calendarConfigure.load(name: LocalData.read(context: LocalSchema.container.mainContext)?.configName, context: DataSchema.container.mainContext) + locationManager.enabled = true + watchLayout.autoSave() + calendarConfigure.autoSave() + calendarConfigure.autoSaveName() } var body: some Scene { WindowGroup { WatchFace() - .environment(\.chineseCalendar, chineseCalendar) - .modelContainer(ThemeData.container) + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) .task { self.update() await updateCountDownRelevantIntents(chineseCalendar: chineseCalendar.copy) @@ -38,8 +49,11 @@ struct Chinendar: App { } func update() { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, - timezone: watchSetting.timezone ?? Calendar.current.timeZone, - location: locationManager.location ?? watchLayout.location) + chineseCalendar.update(time: watchSetting.effectiveTime, + timezone: calendarConfigure.effectiveTimezone, + location: calendarConfigure.location(locationManager: locationManager), + globalMonth: calendarConfigure.globalMonth, + apparentTime: calendarConfigure.apparentTime, + largeHour: calendarConfigure.largeHour) } } diff --git a/iOS/layout.txt b/iOS/layout.txt index cb8edf5..bebb58b 100644 --- a/iOS/layout.txt +++ b/iOS/layout.txt @@ -6,6 +6,7 @@ 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 +startingPhase: zeroRing: 0.0; firstRing: 0.0; secondRing: 0.0; thirdRing: 0.0; fourthRing: 0.0 innerColor: 0xFFFFFFFF backColor: 0xFFFFFFFF majorTickColor: 0x00000000 diff --git a/macOS/Info.plist b/macOS/Info.plist index ef79656..2381b7c 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -24,8 +24,6 @@ $(APP_VERSION) CFBundleVersion $(APP_BUILD) - GroupID - $(TeamIdentifierPrefix)ChineseTime ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/macOS/Layout.swift b/macOS/Layout.swift index 1f18c95..8dd5094 100644 --- a/macOS/Layout.swift +++ b/macOS/Layout.swift @@ -9,7 +9,6 @@ import SwiftUI import Observation @Observable final class WatchLayout: MetaWatchLayout { - static var shared = WatchLayout() struct StatusBar: Equatable { @@ -61,13 +60,9 @@ import Observation 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, includeColor: Bool = true, includeConfig: Bool = true) -> String { - var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor, includeConfig: includeConfig) + override func encode(includeOffset: Bool = true, includeColor: Bool = true) -> String { + var encoded = super.encode(includeOffset: includeOffset, includeColor: includeColor) encoded += "textFont: \(textFont.fontName)\n" encoded += "centerFont: \(centerFont.fontName)\n" encoded += "statusBar: \(statusBar.encode())\n" @@ -99,14 +94,13 @@ import Observation } @Observable class WatchSetting { - static let shared = WatchSetting() enum Selection: String, CaseIterable { - case datetime, location, ringColor, decoration, markColor, layout, themes, documentation + case datetime, location, configs, ringColor, decoration, markColor, layout, themes, documentation } var displayTime: Date? = nil - var timezone: TimeZone? = nil @ObservationIgnored var previousSelection: Selection? = nil - - private init() {} + var effectiveTime: Date { + displayTime ?? .now + } } diff --git a/macOS/Views/Setting.swift b/macOS/Views/Setting.swift index ee2dbc9..f6cc5f1 100644 --- a/macOS/Views/Setting.swift +++ b/macOS/Views/Setting.swift @@ -10,24 +10,27 @@ import WidgetKit import StoreKit struct Setting: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.watchSetting) var watchSetting - @Environment(\.chineseCalendar) var chineseCalendar - @Environment(\.locationManager) var locationManager + @Environment(WatchLayout.self) var watchLayout + @Environment(CalendarConfigure.self) var calendarConfigure + @Environment(WatchSetting.self) var watchSetting + @Environment(ChineseCalendar.self) var chineseCalendar + @Environment(LocationManager.self) var locationManager @State private var selection: WatchSetting.Selection? = .none @State private var columnVisibility = NavigationSplitViewVisibility.automatic @Environment(\.modelContext) private var modelContext @Environment(\.requestReview) var requestReview private var statusState: StatusState { - StatusState(locationManager: locationManager, watchLayout: watchLayout, watchSetting: watchSetting) + StatusState(locationManager: locationManager, watchLayout: watchLayout, calendarConfigure: calendarConfigure, watchSetting: watchSetting) } var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { List(selection: $selection) { Section("時空") { - ForEach([WatchSetting.Selection.datetime, WatchSetting.Selection.location], id: \.self) { selection in + ForEach([WatchSetting.Selection.datetime, + WatchSetting.Selection.location, + WatchSetting.Selection.configs], id: \.self) { selection in buildView(selection: selection) } } @@ -52,6 +55,8 @@ struct Setting: View { Datetime() case .location: Location() + case .configs: + ConfigList() case .ringColor: RingSetting() case .decoration: @@ -103,7 +108,6 @@ struct Setting: View { } .onDisappear { selection = .none - watchLayout.saveDefault(context: modelContext) WidgetCenter.shared.reloadAllTimelines() AppDelegate.instance?.lastReloaded = .now cleanColorPanel() @@ -116,6 +120,8 @@ struct Setting: View { Label("日時", systemImage: "clock") case .location: Label("經緯度", systemImage: "location") + case .configs: + Label("日曆墻", systemImage: "globe") case .ringColor: Label("輪色", systemImage: "pencil.and.outline") case .decoration: @@ -140,5 +146,18 @@ struct Setting: View { } #Preview("Settings") { - Setting() + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + watchLayout.loadStatic() + + return Setting() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) } diff --git a/macOS/Views/WatchFace.swift b/macOS/Views/WatchFace.swift index 6959fbb..6a725fa 100644 --- a/macOS/Views/WatchFace.swift +++ b/macOS/Views/WatchFace.swift @@ -7,10 +7,9 @@ import SwiftUI -@MainActor struct WatchFace: View { - @Environment(\.watchLayout) var watchLayout - @Environment(\.chineseCalendar) var chineseCalendar + @Environment(WatchLayout.self) var watchLayout + @Environment(ChineseCalendar.self) var chineseCalendar @State var entityPresenting = EntitySelection() @State var tapPos: CGPoint? = nil @State var hoverBounds: CGRect = .zero @@ -71,6 +70,12 @@ struct WatchFace: View { } #Preview("WatchFace") { - WatchFace() - .modelContainer(ThemeData.container) + let chineseCalendar = ChineseCalendar() + let watchLayout = WatchLayout() + watchLayout.loadStatic() + + return WatchFace() + .modelContainer(DataSchema.container) + .environment(chineseCalendar) + .environment(watchLayout) } diff --git a/macOS/Views/Welcome.swift b/macOS/Views/Welcome.swift index 338c2b9..36333ab 100644 --- a/macOS/Views/Welcome.swift +++ b/macOS/Views/Welcome.swift @@ -9,50 +9,69 @@ import SwiftUI struct Welcome: View { @Environment(\.dismiss) var dismiss - @Environment(\.watchLayout) var watchLayout + @Environment(WatchLayout.self) var watchLayout var body: some View { - VStack(spacing: 20) { - Icon(watchLayout: watchLayout) - .frame(width: 120, height: 120) - Text("華曆", comment: "Chinendar") - .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") + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + Icon(watchLayout: watchLayout) + .frame(width: 120, height: 120) + Text("華曆", comment: "Chinendar") + .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(.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") + } + } + HStack { + Image(systemName: "wand.and.stars") + .font(.largeTitle) + .frame(width: 70, height: 70) + .foregroundStyle(Color.accentColor) + VStack(alignment: .leading) { + Text("新功能", comment: "Welcome, new features - title") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.headline) + .padding(.vertical, 5) + .padding(.trailing, 5) + Text("新增功能詳情", comment: "Welcome, new features detail") + } } } + .padding(20) } - .padding(20) } } #Preview("Welcome") { - Welcome() + let watchLayout = WatchLayout() + watchLayout.loadStatic() + return Welcome() + .environment(watchLayout) .frame(minWidth: 300, idealWidth: 350, maxWidth: 400, minHeight: 400, idealHeight: 600, maxHeight: 700, alignment: .center) } diff --git a/macOS/layout.txt b/macOS/layout.txt index a6fa120..6186b5b 100644 --- a/macOS/layout.txt +++ b/macOS/layout.txt @@ -6,6 +6,7 @@ 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 +startingPhase: zeroRing: 0.0; firstRing: 0.0; secondRing: 0.0; thirdRing: 0.0; fourthRing: 0.0 innerColor: 0xFFFFFFFF backColor: 0xFFFFFFFF majorTickColor: 0x00000000 diff --git a/macOS/macApp.swift b/macOS/macApp.swift index fac40b7..143a909 100644 --- a/macOS/macApp.swift +++ b/macOS/macApp.swift @@ -6,7 +6,7 @@ // import SwiftUI -@preconcurrency import WidgetKit +import WidgetKit @main struct Chinendar: App { @@ -16,6 +16,7 @@ struct Chinendar: App { WindowGroup { Welcome() .frame(minWidth: 300, idealWidth: 350, maxWidth: 400, minHeight: 400, idealHeight: 600, maxHeight: 700, alignment: .center) + .environment(appDelegate.watchLayout) } .windowResizability(.contentSize) .windowStyle(.hiddenTitleBar) @@ -26,11 +27,12 @@ struct Chinendar: App { 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) + let chineseCalendar = ChineseCalendar() + let locationManager = LocationManager() + let watchLayout = WatchLayout() + let calendarConfigure = CalendarConfigure() + let watchSetting = WatchSetting() + let dataContainer = DataSchema.container var watchPanel: WatchPanel! private var _timer: Timer? var lastReloaded = Date.distantPast @@ -39,8 +41,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { 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() + watchLayout.loadDefault(context: dataContainer.mainContext) + calendarConfigure.load(name: LocalData.read(context: LocalSchema.container.mainContext)?.configName, context: dataContainer.mainContext) + locationManager.enabled = true + watchLayout.autoSave() + calendarConfigure.autoSave() + calendarConfigure.autoSaveName() AppDelegate.instance = self } @@ -48,14 +54,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate { update() watchPanel = { let watchFace = WatchFace() - .environment(\.chineseCalendar, chineseCalendar) - .modelContainer(modelContainer) + .modelContainer(dataContainer) + .environment(chineseCalendar) + .environment(watchLayout) let setting = Setting() .frame(minWidth: 550, maxWidth: 700, minHeight: 350, maxHeight: 500) - .environment(\.chineseCalendar, chineseCalendar) - .modelContainer(modelContainer) + .modelContainer(dataContainer) + .environment(chineseCalendar) + .environment(locationManager) + .environment(watchLayout) + .environment(calendarConfigure) + .environment(watchSetting) - return WatchPanelHosting(watch: watchFace, setting: setting, statusItem: statusItem, isPresented: false) + return WatchPanelHosting(watch: watchFace, setting: setting, statusItem: statusItem, watchLayout: watchLayout, isPresented: false) }() _timer = Timer.scheduledTimer(withTimeInterval: ChineseCalendar.updateInterval, repeats: true) { _ in Task { @MainActor in @@ -69,8 +80,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } func applicationWillTerminate(_ aNotification: Notification) { - watchLayout.saveDefault(context: modelContainer.mainContext) - try? modelContainer.mainContext.save() if lastReloaded.distance(to: .now) > 1800 { // Half Hour WidgetCenter.shared.reloadAllTimelines() } @@ -118,9 +127,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } func update() { - chineseCalendar.update(time: watchSetting.displayTime ?? Date.now, - timezone: watchSetting.timezone ?? Calendar.current.timeZone, - location: locationManager.location ?? watchLayout.location) + chineseCalendar.update(time: watchSetting.effectiveTime, + timezone: calendarConfigure.effectiveTimezone, + location: calendarConfigure.location(locationManager: locationManager), + globalMonth: calendarConfigure.globalMonth, + apparentTime: calendarConfigure.apparentTime, + largeHour: calendarConfigure.largeHour) updateStatusBar(dateText: statusBar(from: chineseCalendar, options: watchLayout)) } } diff --git a/macOS/watchPanel.swift b/macOS/watchPanel.swift index 6795412..39944bd 100644 --- a/macOS/watchPanel.swift +++ b/macOS/watchPanel.swift @@ -10,7 +10,7 @@ import SwiftUI class WatchPanel: NSPanel { private var _isPresented: Bool private let statusItem: NSStatusItem - private let watchLayout = WatchLayout.shared + private let watchLayout: WatchLayout fileprivate let backView: NSVisualEffectView fileprivate let settingButton: OptionView fileprivate let closeButton: OptionView @@ -29,9 +29,10 @@ class WatchPanel: NSPanel { } } - init(statusItem: NSStatusItem, isPresented: Bool) { + init(statusItem: NSStatusItem, watchLayout: WatchLayout, isPresented: Bool) { self._isPresented = isPresented self.statusItem = statusItem + self.watchLayout = watchLayout let blurView = NSVisualEffectView() blurView.blendingMode = .behindWindow @@ -128,10 +129,10 @@ internal final class WatchPanelHosting: Watc private let watchView: NSHostingView private let settingView: NSHostingController - init(watch: WatchView, setting: SettingView, statusItem: NSStatusItem, isPresented: Bool) { + init(watch: WatchView, setting: SettingView, statusItem: NSStatusItem, watchLayout: WatchLayout, isPresented: Bool) { watchView = NSHostingView(rootView: watch) settingView = NSHostingController(rootView: setting) - super.init(statusItem: statusItem, isPresented: isPresented) + super.init(statusItem: statusItem, watchLayout: watchLayout, isPresented: isPresented) settingButton.button.action = #selector(openSetting(_:)) contentView?.addSubview(watchView) }