Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1a74f2b3e9
|
|||
|
97a5e951e1
|
|||
|
9071ac44fe
|
|||
|
6abee8d8bd
|
|||
|
8cf03683dc
|
|||
|
f34d80b7d4
|
|||
|
b6d7e52148
|
|||
|
978b7b32fd
|
|||
|
35a9c9ff4b
|
|||
|
e5cb296367
|
|||
|
cf3a2b6340
|
|||
|
f568baf14d
|
|||
|
703335429a
|
@@ -17,6 +17,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
|
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
|
||||||
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterFragmentActivity()
|
||||||
{
|
{
|
||||||
private val CHANNEL = "dev.solsynth.solian/notifications"
|
private val CHANNEL = "dev.solsynth.solian/notifications"
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,7 @@
|
|||||||
"checkInResultT2": "中平",
|
"checkInResultT2": "中平",
|
||||||
"checkInResultT3": "吉",
|
"checkInResultT3": "吉",
|
||||||
"checkInResultT4": "大吉",
|
"checkInResultT4": "大吉",
|
||||||
|
"checkInResultT5": "特殊",
|
||||||
"accountProfileView": "查看个人资料",
|
"accountProfileView": "查看个人资料",
|
||||||
"unspecified": "未指定",
|
"unspecified": "未指定",
|
||||||
"added": "已添加",
|
"added": "已添加",
|
||||||
|
|||||||
@@ -455,6 +455,7 @@
|
|||||||
"checkInResultT2": "Mid",
|
"checkInResultT2": "Mid",
|
||||||
"checkInResultT3": "Good",
|
"checkInResultT3": "Good",
|
||||||
"checkInResultT4": "Best",
|
"checkInResultT4": "Best",
|
||||||
|
"checkInResultT5": "Special",
|
||||||
"accountProfileView": "View Profile",
|
"accountProfileView": "View Profile",
|
||||||
"unspecified": "Unspecified",
|
"unspecified": "Unspecified",
|
||||||
"added": "Added",
|
"added": "Added",
|
||||||
|
|||||||
@@ -455,6 +455,7 @@
|
|||||||
"checkInResultT2": "中平",
|
"checkInResultT2": "中平",
|
||||||
"checkInResultT3": "吉",
|
"checkInResultT3": "吉",
|
||||||
"checkInResultT4": "大吉",
|
"checkInResultT4": "大吉",
|
||||||
|
"checkInResultT5": "特殊",
|
||||||
"accountProfileView": "查看個人資料",
|
"accountProfileView": "查看個人資料",
|
||||||
"unspecified": "未指定",
|
"unspecified": "未指定",
|
||||||
"added": "已添加",
|
"added": "已添加",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
|
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
|
||||||
|
7301DB032F08D99C008390F3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB022F08D99C008390F3 /* WidgetKit.framework */; };
|
||||||
|
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB042F08D99C008390F3 /* SwiftUI.framework */; };
|
||||||
|
7301DB102F08D99D008390F3 /* SolianWidgetExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
||||||
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -36,6 +39,13 @@
|
|||||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
remoteInfo = Runner;
|
remoteInfo = Runner;
|
||||||
};
|
};
|
||||||
|
7301DB0E2F08D99D008390F3 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 7301DB002F08D99C008390F3;
|
||||||
|
remoteInfo = SolianWidgetExtensionExtension;
|
||||||
|
};
|
||||||
73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */ = {
|
73ACDFC12E3D0E6100B63535 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
@@ -77,6 +87,7 @@
|
|||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 13;
|
dstSubfolderSpec = 13;
|
||||||
files = (
|
files = (
|
||||||
|
7301DB102F08D99D008390F3 /* SolianWidgetExtensionExtension.appex in Embed Foundation Extensions */,
|
||||||
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */,
|
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */,
|
||||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */,
|
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */,
|
||||||
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
|
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
|
||||||
@@ -117,6 +128,10 @@
|
|||||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianWidgetExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
7301DB022F08D99C008390F3 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
|
7301DB042F08D99C008390F3 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
|
7301DB162F08D9A5008390F3 /* SolianWidgetExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SolianWidgetExtensionExtension.entitlements; sourceTree = "<group>"; };
|
||||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -151,6 +166,13 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
7301DB142F08D99D008390F3 /* Exceptions for "SolianWidgetExtension" folder in "SolianWidgetExtensionExtension" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */;
|
||||||
|
};
|
||||||
73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */ = {
|
73ACDFCA2E3D0E6100B63535 /* Exceptions for "SolianBroadcastExtension" folder in "SolianBroadcastExtension" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
@@ -177,16 +199,23 @@
|
|||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
CloudFile.swift,
|
CloudFile.swift,
|
||||||
DataExchange.swift,
|
DataExchange.swift,
|
||||||
|
GroupDefaultSync.swift,
|
||||||
);
|
);
|
||||||
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
||||||
};
|
};
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
7301DB062F08D99C008390F3 /* SolianWidgetExtension */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
|
7301DB142F08D99D008390F3 /* Exceptions for "SolianWidgetExtension" folder in "SolianWidgetExtensionExtension" target */,
|
||||||
);
|
);
|
||||||
|
path = SolianWidgetExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = "Solian Watch App";
|
path = "Solian Watch App";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -233,6 +262,15 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7301DAFE2F08D99C008390F3 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */,
|
||||||
|
7301DB032F08D99C008390F3 /* WidgetKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -295,6 +333,8 @@
|
|||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
||||||
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
||||||
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
|
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
|
||||||
|
7301DB022F08D99C008390F3 /* WidgetKit.framework */,
|
||||||
|
7301DB042F08D99C008390F3 /* SwiftUI.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -341,12 +381,14 @@
|
|||||||
97C146E51CF9000F007C117D = {
|
97C146E51CF9000F007C117D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
7301DB162F08D9A5008390F3 /* SolianWidgetExtensionExtension.entitlements */,
|
||||||
9740EEB11CF90186004384FC /* Flutter */,
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
||||||
|
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
91E124CE95BCB4DCD890160D /* Pods */,
|
91E124CE95BCB4DCD890160D /* Pods */,
|
||||||
@@ -364,6 +406,7 @@
|
|||||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
||||||
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
|
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
|
||||||
|
7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -408,6 +451,26 @@
|
|||||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
|
7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 7301DB152F08D99D008390F3 /* Build configuration list for PBXNativeTarget "SolianWidgetExtensionExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
7301DAFD2F08D99C008390F3 /* Sources */,
|
||||||
|
7301DAFE2F08D99C008390F3 /* Frameworks */,
|
||||||
|
7301DAFF2F08D99C008390F3 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
|
||||||
|
);
|
||||||
|
name = SolianWidgetExtensionExtension;
|
||||||
|
productName = SolianWidgetExtensionExtension;
|
||||||
|
productReference = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
|
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
|
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
|
||||||
@@ -515,6 +578,7 @@
|
|||||||
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
|
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
|
||||||
73C305D72E0BE878009035B9 /* PBXTargetDependency */,
|
73C305D72E0BE878009035B9 /* PBXTargetDependency */,
|
||||||
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */,
|
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */,
|
||||||
|
7301DB0F2F08D99D008390F3 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
73268D272DEB012A0076E970 /* Services */,
|
73268D272DEB012A0076E970 /* Services */,
|
||||||
@@ -531,7 +595,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2600;
|
LastSwiftUpdateCheck = 2620;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -539,6 +603,9 @@
|
|||||||
CreatedOnToolsVersion = 14.0;
|
CreatedOnToolsVersion = 14.0;
|
||||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
};
|
};
|
||||||
|
7301DB002F08D99C008390F3 = {
|
||||||
|
CreatedOnToolsVersion = 26.2;
|
||||||
|
};
|
||||||
7310A7D32EB10962002C0FD3 = {
|
7310A7D32EB10962002C0FD3 = {
|
||||||
CreatedOnToolsVersion = 26.0.1;
|
CreatedOnToolsVersion = 26.0.1;
|
||||||
};
|
};
|
||||||
@@ -563,6 +630,11 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
"zh-Hans",
|
||||||
|
es,
|
||||||
|
ja,
|
||||||
|
ko,
|
||||||
|
"zh-Hant",
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
@@ -576,6 +648,7 @@
|
|||||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
|
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
|
||||||
|
7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -588,6 +661,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7301DAFF2F08D99C008390F3 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -677,10 +757,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -738,10 +822,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -792,10 +880,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
||||||
@@ -852,6 +944,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7301DAFD2F08D99C008390F3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -898,6 +997,11 @@
|
|||||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
7301DB0F2F08D99D008390F3 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 7301DB002F08D99C008390F3 /* SolianWidgetExtensionExtension */;
|
||||||
|
targetProxy = 7301DB0E2F08D99D008390F3 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */ = {
|
73ACDFC22E3D0E6100B63535 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */;
|
target = 73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */;
|
||||||
@@ -1080,6 +1184,138 @@
|
|||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
|
7301DB112F08D99D008390F3 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
7301DB122F08D99D008390F3 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
7301DB132F08D99D008390F3 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SolianWidgetExtensionExtension.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianWidgetExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */;
|
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */;
|
||||||
@@ -1785,6 +2021,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
7301DB152F08D99D008390F3 /* Build configuration list for PBXNativeTarget "SolianWidgetExtensionExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
7301DB112F08D99D008390F3 /* Debug */,
|
||||||
|
7301DB122F08D99D008390F3 /* Release */,
|
||||||
|
7301DB132F08D99D008390F3 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
|
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||||
|
BuildableName = "Solian Watch App.app"
|
||||||
|
BlueprintName = "Solian Watch App"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||||
|
BuildableName = "Solian Watch App.app"
|
||||||
|
BlueprintName = "Solian Watch App"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||||
|
BuildableName = "Solian Watch App.app"
|
||||||
|
BlueprintName = "Solian Watch App"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "73ACDFAA2E3D0E6100B63535"
|
||||||
|
BuildableName = "SolianBroadcastExtension.appex"
|
||||||
|
BlueprintName = "SolianBroadcastExtension"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "73CDD6792DEC00480059D95D"
|
||||||
|
BuildableName = "SolianNotificationService.appex"
|
||||||
|
BlueprintName = "SolianNotificationService"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "73C305CD2E0BE878009035B9"
|
||||||
|
BuildableName = "SolianShareExtension.appex"
|
||||||
|
BlueprintName = "SolianShareExtension"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2620"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7301DB002F08D99C008390F3"
|
||||||
|
BuildableName = "SolianWidgetExtensionExtension.appex"
|
||||||
|
BlueprintName = "SolianWidgetExtensionExtension"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||||
|
BuildableName = "Solian Watch App.app"
|
||||||
|
BlueprintName = "Solian Watch App"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
|
import WidgetKit
|
||||||
import UIKit
|
import UIKit
|
||||||
import WatchConnectivity
|
import WatchConnectivity
|
||||||
|
|
||||||
@@ -11,6 +12,9 @@ import WatchConnectivity
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
syncDefaultsToGroup()
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
let replyableMessageCategory = UNNotificationCategory(
|
let replyableMessageCategory = UNNotificationCategory(
|
||||||
@@ -29,6 +33,9 @@ import WatchConnectivity
|
|||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
|
// Setup widget sync method channel
|
||||||
|
setupWidgetSyncChannel()
|
||||||
|
|
||||||
// Always initialize and retain a strong reference
|
// Always initialize and retain a strong reference
|
||||||
if WCSession.isSupported() {
|
if WCSession.isSupported() {
|
||||||
AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared
|
AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared
|
||||||
@@ -38,6 +45,30 @@ import WatchConnectivity
|
|||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupWidgetSyncChannel() {
|
||||||
|
let controller = window?.rootViewController as? FlutterViewController
|
||||||
|
let channel = FlutterMethodChannel(name: "dev.solsynth.solian/widget", binaryMessenger: controller!.binaryMessenger)
|
||||||
|
|
||||||
|
channel.setMethodCallHandler { [weak self] (call, result) in
|
||||||
|
if call.method == "syncToWidget" {
|
||||||
|
syncDefaultsToGroup()
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
result(true)
|
||||||
|
} else {
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
|
syncDefaultsToGroup()
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
syncDefaultsToGroup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
|
|||||||
41
ios/Runner/Services/GroupDefaultSync.swift
Normal file
41
ios/Runner/Services/GroupDefaultSync.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// GroupDefaultSync.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2026/1/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private let flutterKeyPrefix = "flutter."
|
||||||
|
|
||||||
|
private let flutterKeysToSync: [String] = [
|
||||||
|
"dyn_user_tk",
|
||||||
|
"app_server_url"
|
||||||
|
]
|
||||||
|
|
||||||
|
func syncDefaultsToGroup() {
|
||||||
|
print("[iOS] syncDefaultsToGroup() called")
|
||||||
|
|
||||||
|
let standard = UserDefaults.standard
|
||||||
|
let shared = UserDefaults(suiteName: "group.solsynth.solian")
|
||||||
|
|
||||||
|
guard let shared else {
|
||||||
|
print("[iOS] App Group UserDefaults not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in flutterKeysToSync {
|
||||||
|
let prefixedKey = key.starts(with: flutterKeyPrefix) ? key : flutterKeyPrefix + key
|
||||||
|
|
||||||
|
if let value = standard.object(forKey: prefixedKey) {
|
||||||
|
print("[iOS] Syncing key to App Group: \(prefixedKey)")
|
||||||
|
shared.set(value, forKey: prefixedKey)
|
||||||
|
} else {
|
||||||
|
print("[iOS] Key \(prefixedKey) was not found in the app data, skipping...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.synchronize()
|
||||||
|
print("[iOS] Sync completed")
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "icon-dark.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "icon 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
21
ios/SolianWidgetExtension/Assets.xcassets/CloudyLamb.imageset/Contents.json
vendored
Normal file
21
ios/SolianWidgetExtension/Assets.xcassets/CloudyLamb.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/SolianWidgetExtension/Assets.xcassets/CloudyLamb.imageset/icon.png
vendored
Normal file
BIN
ios/SolianWidgetExtension/Assets.xcassets/CloudyLamb.imageset/icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
21
ios/SolianWidgetExtension/Assets.xcassets/CloudyLambDark.imageset/Contents.json
vendored
Normal file
21
ios/SolianWidgetExtension/Assets.xcassets/CloudyLambDark.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-dark.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/SolianWidgetExtension/Assets.xcassets/CloudyLambDark.imageset/icon-dark.png
vendored
Normal file
BIN
ios/SolianWidgetExtension/Assets.xcassets/CloudyLambDark.imageset/icon-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
6
ios/SolianWidgetExtension/Assets.xcassets/Contents.json
Normal file
6
ios/SolianWidgetExtension/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
11
ios/SolianWidgetExtension/Info.plist
Normal file
11
ios/SolianWidgetExtension/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
711
ios/SolianWidgetExtension/SolianCheckInWidget.swift
Normal file
711
ios/SolianWidgetExtension/SolianCheckInWidget.swift
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
//
|
||||||
|
// SolianWidgetExtension.swift
|
||||||
|
// SolianWidgetExtension
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2026/1/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CheckInTip: Codable {
|
||||||
|
let isPositive: Bool
|
||||||
|
let title: String
|
||||||
|
let content: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case isPositive = "is_positive"
|
||||||
|
case title
|
||||||
|
case content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInAccount: Codable {
|
||||||
|
let id: String
|
||||||
|
let nick: String?
|
||||||
|
let profile: CheckInProfile?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInProfile: Codable {
|
||||||
|
let picture: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInResult: Codable {
|
||||||
|
let id: String
|
||||||
|
let level: Int
|
||||||
|
let rewardPoints: Int
|
||||||
|
let rewardExperience: Int
|
||||||
|
let tips: [CheckInTip]
|
||||||
|
let accountId: String
|
||||||
|
let account: CheckInAccount?
|
||||||
|
let createdAt: String
|
||||||
|
let updatedAt: String
|
||||||
|
let deletedAt: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case level
|
||||||
|
case rewardPoints = "reward_points"
|
||||||
|
case rewardExperience = "reward_experience"
|
||||||
|
case tips
|
||||||
|
case accountId = "account_id"
|
||||||
|
case account
|
||||||
|
case createdAt = "created_at"
|
||||||
|
case updatedAt = "updated_at"
|
||||||
|
case deletedAt = "deleted_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdDate: Date? {
|
||||||
|
ISO8601DateFormatter().date(from: createdAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotableDay: Codable {
|
||||||
|
let date: String
|
||||||
|
let localName: String
|
||||||
|
let globalName: String
|
||||||
|
let countryCode: String?
|
||||||
|
let localizableKey: String?
|
||||||
|
let holidays: [Int]?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case date
|
||||||
|
case localName = "local_name"
|
||||||
|
case globalName = "global_name"
|
||||||
|
case countryCode = "country_code"
|
||||||
|
case localizableKey = "localizable_key"
|
||||||
|
case holidays
|
||||||
|
}
|
||||||
|
|
||||||
|
var notableDate: Date? {
|
||||||
|
ISO8601DateFormatter().date(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isToday: Bool {
|
||||||
|
guard let notableDate = notableDate else { return false }
|
||||||
|
let calendar = Calendar.current
|
||||||
|
return calendar.isDateInToday(notableDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckInService {
|
||||||
|
private let networkService = WidgetNetworkService()
|
||||||
|
|
||||||
|
func fetchCheckInResult() async throws -> CheckInResult? {
|
||||||
|
return try await networkService.makeRequest(path: "/pass/accounts/me/check-in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotableDayService {
|
||||||
|
private let networkService = WidgetNetworkService()
|
||||||
|
|
||||||
|
func fetchRecentNotableDay() async throws -> NotableDay? {
|
||||||
|
print("[WidgetKit] [NotableDayService] Fetching recent notable day...")
|
||||||
|
do {
|
||||||
|
let result: [NotableDay]? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
|
||||||
|
print("[WidgetKit] [NotableDayService] Result: \(String(describing: result))")
|
||||||
|
|
||||||
|
guard let result = result else {
|
||||||
|
print("[WidgetKit] [NotableDayService] Result is nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[WidgetKit] [NotableDayService] Result count: \(result.count)")
|
||||||
|
|
||||||
|
guard result.isEmpty == false else {
|
||||||
|
print("[WidgetKit] [NotableDayService] No notable days found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstDay = result.first!
|
||||||
|
print("[WidgetKit] [NotableDayService] First notable day: \(firstDay.localName), date: \(firstDay.date)")
|
||||||
|
|
||||||
|
return firstDay
|
||||||
|
} catch let decodingError as DecodingError {
|
||||||
|
print("[WidgetKit] [NotableDayService] Decoding error, trying as single object...")
|
||||||
|
print("[WidgetKit] [NotableDayService] Error: \(decodingError.localizedDescription)")
|
||||||
|
|
||||||
|
switch decodingError {
|
||||||
|
case .typeMismatch(let type, let context):
|
||||||
|
print("[WidgetKit] [NotableDayService] Type mismatch: expected \(type), context: \(context.debugDescription)")
|
||||||
|
case .valueNotFound(let type, let context):
|
||||||
|
print("[WidgetKit] [NotableDayService] Value not found: type \(type), context: \(context.debugDescription)")
|
||||||
|
case .keyNotFound(let key, let context):
|
||||||
|
print("[WidgetKit] [NotableDayService] Key not found: \(key), context: \(context.debugDescription)")
|
||||||
|
case .dataCorrupted(let context):
|
||||||
|
print("[WidgetKit] [NotableDayService] Data corrupted: \(context.debugDescription)")
|
||||||
|
@unknown default:
|
||||||
|
print("[WidgetKit] [NotableDayService] Unknown decoding error")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let singleResult: NotableDay? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
|
||||||
|
print("[WidgetKit] [NotableDayService] Single object decode succeeded: \(singleResult?.localName ?? "nil")")
|
||||||
|
return singleResult
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] [NotableDayService] Single object decode also failed: \(error.localizedDescription)")
|
||||||
|
throw decodingError
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] [NotableDayService] Error fetching notable day: \(error.localizedDescription)")
|
||||||
|
print("[WidgetKit] [NotableDayService] Error type: \(type(of: error))")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let result: CheckInResult?
|
||||||
|
let notableDay: NotableDay?
|
||||||
|
let error: String?
|
||||||
|
let isLoading: Bool
|
||||||
|
|
||||||
|
static func placeholder() -> CheckInEntry {
|
||||||
|
CheckInEntry(date: Date(), result: nil, notableDay: nil, error: nil, isLoading: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Provider: TimelineProvider {
|
||||||
|
private let apiService = CheckInService()
|
||||||
|
|
||||||
|
func placeholder(in context: Context) -> CheckInEntry {
|
||||||
|
CheckInEntry.placeholder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||||
|
Task {
|
||||||
|
print("[WidgetKit] [Provider] Getting snapshot...")
|
||||||
|
async let checkInResult = try? await apiService.fetchCheckInResult()
|
||||||
|
async let notableDay = try? await NotableDayService().fetchRecentNotableDay()
|
||||||
|
|
||||||
|
let result = try? await checkInResult
|
||||||
|
let day = try? await notableDay
|
||||||
|
|
||||||
|
print("[WidgetKit] [Provider] Snapshot - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
|
||||||
|
|
||||||
|
let entry = CheckInEntry(date: Date(), result: result, notableDay: day, error: nil, isLoading: false)
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||||
|
Task {
|
||||||
|
let currentDate = Date()
|
||||||
|
print("[WidgetKit] [Provider] Getting timeline at \(currentDate)...")
|
||||||
|
|
||||||
|
do {
|
||||||
|
async let checkInResult = try await apiService.fetchCheckInResult()
|
||||||
|
async let notableDay = try await NotableDayService().fetchRecentNotableDay()
|
||||||
|
|
||||||
|
let result = try await checkInResult
|
||||||
|
let day = try await notableDay
|
||||||
|
|
||||||
|
print("[WidgetKit] [Provider] Timeline - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
|
||||||
|
|
||||||
|
let entry = CheckInEntry(date: currentDate, result: result, notableDay: day, error: nil, isLoading: false)
|
||||||
|
|
||||||
|
let nextUpdateDate: Date
|
||||||
|
if let result = result, let createdDate = result.createdDate {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: createdDate) {
|
||||||
|
nextUpdateDate = min(tomorrow, calendar.date(byAdding: .hour, value: 1, to: currentDate)!)
|
||||||
|
} else {
|
||||||
|
nextUpdateDate = calendar.date(byAdding: .hour, value: 1, to: currentDate)!
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[WidgetKit] [Provider] Next update at: \(nextUpdateDate)")
|
||||||
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate))
|
||||||
|
completion(timeline)
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] [Provider] Error in getTimeline: \(error.localizedDescription)")
|
||||||
|
let entry = CheckInEntry(date: currentDate, result: nil, notableDay: nil, error: error.localizedDescription, isLoading: false)
|
||||||
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)!
|
||||||
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||||
|
completion(timeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInWidgetEntryView: View {
|
||||||
|
var entry: Provider.Entry
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let result = entry.result {
|
||||||
|
CheckedInView(result: result, notableDay: entry.notableDay)
|
||||||
|
} else if entry.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
} else if let error = entry.error {
|
||||||
|
ErrorView(error: error)
|
||||||
|
} else {
|
||||||
|
NotCheckedInView(notableDay: entry.notableDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getLevelName(for level: Int) -> String {
|
||||||
|
let key = "checkInResultT\(level)"
|
||||||
|
return NSLocalizedString(key, comment: "Check-in result level name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func NotableDayView(notableDay: NotableDay) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
if !notableDay.isToday {
|
||||||
|
Text(NSLocalizedString("notableDayUpcoming", comment: "Upcoming"))
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
HStack(spacing: isAccessory ? 8 : 6) {
|
||||||
|
Image(systemName: "sparkles")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.font(isAccessory ? .caption : .subheadline)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
if notableDay.isToday {
|
||||||
|
Text(String(format: NSLocalizedString("notableDayToday", comment: "{name} is today!"), notableDay.localName))
|
||||||
|
.font(isAccessory ? .caption : .footnote)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(2)
|
||||||
|
} else {
|
||||||
|
if let notableDate = notableDay.notableDate {
|
||||||
|
let dateString = isCompact ? formatDateCompact(notableDate) : formatDateRegular(notableDate)
|
||||||
|
Text(String(format: NSLocalizedString("notableDayIs", comment: "{date} is {name}"), dateString, notableDay.localName))
|
||||||
|
.font(isAccessory ? .caption : .footnote)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(2)
|
||||||
|
} else {
|
||||||
|
Text(notableDay.localName)
|
||||||
|
.font(isAccessory ? .caption : .footnote)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if notableDay.isToday && !isAccessory {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text(NSLocalizedString("today", comment: "Today"))
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isCompact: Bool {
|
||||||
|
family == .systemSmall || isAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDateCompact(_ date: Date) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "M/d"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDateRegular(_ date: Date) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "MMM d, yyyy"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func CheckedInView(result: CheckInResult, notableDay: NotableDay?) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://dashboard")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.font(isAccessory ? .caption : .title3)
|
||||||
|
Text(getLevelName(for: result.level))
|
||||||
|
.font(isAccessory ? .caption2 : .headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.tips.isEmpty {
|
||||||
|
if isAccessory {
|
||||||
|
let positiveTips = result.tips.filter { $0.isPositive }
|
||||||
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
||||||
|
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
if let positiveTip = positiveTips.first {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "hand.thumbsup.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(positiveTip.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let negativeTip = negativeTips.first {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "hand.thumbsdown.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(negativeTip.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else if family == .systemSmall {
|
||||||
|
let positiveTips = result.tips.filter { $0.isPositive }
|
||||||
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
if let positiveTip = positiveTips.first {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "hand.thumbsup.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.green.opacity(0.8))
|
||||||
|
Text(positiveTip.title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let negativeTip = negativeTips.first {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "hand.thumbsdown.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red.opacity(0.8))
|
||||||
|
Text(negativeTip.title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let positiveTips = result.tips.filter { $0.isPositive }
|
||||||
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
if !positiveTips.isEmpty {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "hand.thumbsup.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.green.opacity(0.8))
|
||||||
|
ForEach(Array(positiveTips.prefix(3)), id: \.title) { tip in
|
||||||
|
Text(tip.title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
if tip.title != positiveTips.last?.title {
|
||||||
|
Text("•")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !negativeTips.isEmpty {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "hand.thumbsdown.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red.opacity(0.8))
|
||||||
|
ForEach(Array(negativeTips.prefix(3)), id: \.title) { tip in
|
||||||
|
Text(tip.title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
if tip.title != negativeTips.last?.title {
|
||||||
|
Text("•")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !isAccessory && family != .systemSmall {
|
||||||
|
Text("No fortune today")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let notableDay = notableDay {
|
||||||
|
NotableDayView(notableDay: notableDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if family == .systemLarge {
|
||||||
|
Spacer()
|
||||||
|
WidgetFooter()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func WidgetFooter() -> some View {
|
||||||
|
HStack {
|
||||||
|
Text("Solian")
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isAccessory: Bool {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
if case .accessoryRectangular = family {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func NotCheckedInView(notableDay: NotableDay?) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://dashboard")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(isAccessory ? .caption : .title3)
|
||||||
|
Text(NSLocalizedString("checkIn", comment: "Check In"))
|
||||||
|
.font(isAccessory ? .caption2 : .headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Text(NSLocalizedString("tapToCheckIn", comment: "Tap to check in today"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if let notableDay = notableDay {
|
||||||
|
NotableDayView(notableDay: notableDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
WidgetFooter()
|
||||||
|
} else if let notableDay = notableDay {
|
||||||
|
NotableDayView(notableDay: notableDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func LoadingView() -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(isAccessory ? 0.6 : 0.8)
|
||||||
|
Text(NSLocalizedString("loading", comment: "Loading..."))
|
||||||
|
.font(isAccessory ? .caption2 : .caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Spacer()
|
||||||
|
WidgetFooter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 0 : 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func ErrorView(error: String) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://dashboard")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(isAccessory ? .caption : .title3)
|
||||||
|
Text(NSLocalizedString("error", comment: "Error"))
|
||||||
|
.font(isAccessory ? .caption2 : .headline)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(error)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
WidgetFooter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInWidgetRootView: View {
|
||||||
|
var entry: Provider.Entry
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
ZStack {
|
||||||
|
CheckInWidgetEntryView(entry: entry)
|
||||||
|
|
||||||
|
if entry.result != nil || entry.notableDay != nil {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(
|
||||||
|
width: geometry.size.width * 0.9,
|
||||||
|
height: geometry.size.width * 0.9
|
||||||
|
)
|
||||||
|
.opacity(0.18)
|
||||||
|
.mask(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [
|
||||||
|
Color.white,
|
||||||
|
Color.white,
|
||||||
|
Color.clear
|
||||||
|
]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.position(
|
||||||
|
x: geometry.size.width * 0.9,
|
||||||
|
y: 20
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.fill.tertiary, for: .widget)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
} else {
|
||||||
|
CheckInWidgetEntryView(entry: entry)
|
||||||
|
.padding()
|
||||||
|
.background()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SolianCheckInWidget: Widget {
|
||||||
|
let kind: String = "SolianCheckInWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||||
|
CheckInWidgetRootView(entry: entry)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Check In")
|
||||||
|
.description("View your daily check-in status")
|
||||||
|
.supportedFamilies(supportedFamilies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var supportedFamilies: [WidgetFamily] {
|
||||||
|
#if os(iOS)
|
||||||
|
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular]
|
||||||
|
#else
|
||||||
|
return [.systemSmall, .systemMedium, .systemLarge]
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemSmall) {
|
||||||
|
SolianCheckInWidget()
|
||||||
|
} timeline: {
|
||||||
|
CheckInEntry(date: .now, result: nil, notableDay: nil, error: nil, isLoading: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemMedium) {
|
||||||
|
SolianCheckInWidget()
|
||||||
|
} timeline: {
|
||||||
|
CheckInEntry(
|
||||||
|
date: .now,
|
||||||
|
result: CheckInResult(
|
||||||
|
id: "test-id",
|
||||||
|
level: 2,
|
||||||
|
rewardPoints: 10,
|
||||||
|
rewardExperience: 100,
|
||||||
|
tips: [
|
||||||
|
CheckInTip(isPositive: true, title: "Good Luck", content: "Great day"),
|
||||||
|
CheckInTip(isPositive: true, title: "Creative", content: "Inspiration"),
|
||||||
|
CheckInTip(isPositive: false, title: "Shopping", content: "Expensive")
|
||||||
|
],
|
||||||
|
accountId: "account-id",
|
||||||
|
account: nil,
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
notableDay: NotableDay(
|
||||||
|
date: ISO8601DateFormatter().string(from: Calendar.current.date(byAdding: .day, value: 5, to: Date())!),
|
||||||
|
localName: "Christmas",
|
||||||
|
globalName: "Christmas",
|
||||||
|
countryCode: nil,
|
||||||
|
localizableKey: nil,
|
||||||
|
holidays: []
|
||||||
|
),
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
#Preview(as: .accessoryRectangular) {
|
||||||
|
SolianCheckInWidget()
|
||||||
|
} timeline: {
|
||||||
|
CheckInEntry(
|
||||||
|
date: .now,
|
||||||
|
result: CheckInResult(
|
||||||
|
id: "test-id",
|
||||||
|
level: 4,
|
||||||
|
rewardPoints: 50,
|
||||||
|
rewardExperience: 500,
|
||||||
|
tips: [
|
||||||
|
CheckInTip(isPositive: true, title: "Lucky", content: "Great fortune"),
|
||||||
|
CheckInTip(isPositive: true, title: "Success", content: "Opportunity")
|
||||||
|
],
|
||||||
|
accountId: "account-id",
|
||||||
|
account: nil,
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
notableDay: NotableDay(
|
||||||
|
date: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
localName: "New Year",
|
||||||
|
globalName: "New Year",
|
||||||
|
countryCode: nil,
|
||||||
|
localizableKey: nil,
|
||||||
|
holidays: []
|
||||||
|
),
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
849
ios/SolianWidgetExtension/SolianNotificationWidget.swift
Normal file
849
ios/SolianWidgetExtension/SolianNotificationWidget.swift
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
//
|
||||||
|
// SolianNotificationWidget.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2026/1/4.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Notification Widget
|
||||||
|
|
||||||
|
struct NotificationMeta: Codable {
|
||||||
|
let pfp: String?
|
||||||
|
let images: [String]?
|
||||||
|
let actionUri: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pfp
|
||||||
|
case images
|
||||||
|
case actionUri = "action_uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnNotification: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let topic: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let content: String
|
||||||
|
let meta: NotificationMeta?
|
||||||
|
let priority: Int
|
||||||
|
let viewedAt: String?
|
||||||
|
let accountId: String
|
||||||
|
let createdAt: String
|
||||||
|
let updatedAt: String
|
||||||
|
let deletedAt: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case topic
|
||||||
|
case title
|
||||||
|
case subtitle
|
||||||
|
case content
|
||||||
|
case meta
|
||||||
|
case priority
|
||||||
|
case viewedAt = "viewed_at"
|
||||||
|
case accountId = "account_id"
|
||||||
|
case createdAt = "created_at"
|
||||||
|
case updatedAt = "updated_at"
|
||||||
|
case deletedAt = "deleted_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdDate: Date? {
|
||||||
|
ISO8601DateFormatter().date(from: createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isUnread: Bool {
|
||||||
|
return viewedAt == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTopicIcon() -> String {
|
||||||
|
switch topic {
|
||||||
|
case "post.replies":
|
||||||
|
return "arrow.uturn.backward"
|
||||||
|
case "wallet.transactions":
|
||||||
|
return "wallet.pass"
|
||||||
|
case "relationships.friends.request":
|
||||||
|
return "person.badge.plus"
|
||||||
|
case "invites.chat":
|
||||||
|
return "message.badge"
|
||||||
|
case "invites.realm":
|
||||||
|
return "globe"
|
||||||
|
case "auth.login":
|
||||||
|
return "arrow.right.square"
|
||||||
|
case "posts.new":
|
||||||
|
return "doc.badge.plus"
|
||||||
|
case "wallet.orders.paid":
|
||||||
|
return "baggage"
|
||||||
|
case "posts.reactions.new":
|
||||||
|
return "face.smiling"
|
||||||
|
default:
|
||||||
|
return "bell"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let notifications: [SnNotification]?
|
||||||
|
let unreadCount: Int
|
||||||
|
let error: String?
|
||||||
|
let isLoading: Bool
|
||||||
|
|
||||||
|
static func placeholder() -> NotificationEntry {
|
||||||
|
NotificationEntry(date: Date(), notifications: nil, unreadCount: 0, error: nil, isLoading: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
private let networkService = WidgetNetworkService()
|
||||||
|
|
||||||
|
private lazy var session: URLSession = {
|
||||||
|
let configuration = URLSessionConfiguration.ephemeral
|
||||||
|
configuration.timeoutIntervalForRequest = 10.0
|
||||||
|
configuration.timeoutIntervalForResource = 10.0
|
||||||
|
configuration.waitsForConnectivity = false
|
||||||
|
return URLSession(configuration: configuration)
|
||||||
|
}()
|
||||||
|
|
||||||
|
func fetchRecentNotifications(take: Int = 5) async throws -> [SnNotification] {
|
||||||
|
if take == 0 {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let token = networkService.token else {
|
||||||
|
throw RemoteError.missingCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseURL = networkService.baseURL
|
||||||
|
guard let url = URL(string: "\(baseURL)/ring/notifications?unmark=true&take=\(take)") else {
|
||||||
|
throw RemoteError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.timeoutInterval = 10.0
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw RemoteError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpResponse.statusCode {
|
||||||
|
case 200...299:
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let notifications = try decoder.decode([SnNotification].self, from: data)
|
||||||
|
return notifications
|
||||||
|
case 404:
|
||||||
|
return []
|
||||||
|
default:
|
||||||
|
throw RemoteError.httpError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUnreadCount() async throws -> Int {
|
||||||
|
guard let token = networkService.token else {
|
||||||
|
throw RemoteError.missingCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseURL = networkService.baseURL
|
||||||
|
guard let url = URL(string: "\(baseURL)/ring/notifications/count") else {
|
||||||
|
throw RemoteError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.timeoutInterval = 10.0
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw RemoteError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpResponse.statusCode {
|
||||||
|
case 200...299:
|
||||||
|
if let count = try? JSONSerialization.jsonObject(with: data) as? Int {
|
||||||
|
return count
|
||||||
|
} else if let count = try? JSONSerialization.jsonObject(with: data) as? Double {
|
||||||
|
return Int(count)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case 404:
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
throw RemoteError.httpError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationProvider: TimelineProvider {
|
||||||
|
private let notificationService = NotificationService()
|
||||||
|
|
||||||
|
func placeholder(in context: Context) -> NotificationEntry {
|
||||||
|
NotificationEntry.placeholder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (NotificationEntry) -> ()) {
|
||||||
|
Task {
|
||||||
|
print("[WidgetKit] [NotificationProvider] Getting snapshot...")
|
||||||
|
async let notifications = try? await notificationService.fetchRecentNotifications(take: 5)
|
||||||
|
async let unreadCount = try? await notificationService.fetchUnreadCount()
|
||||||
|
|
||||||
|
let notifs = try? await notifications
|
||||||
|
let unread = (try? await unreadCount) ?? 0
|
||||||
|
|
||||||
|
print("[WidgetKit] [NotificationProvider] Snapshot - Notifications: \(notifs?.count ?? 0), Unread: \(unread)")
|
||||||
|
|
||||||
|
let entry = NotificationEntry(date: Date(), notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||||
|
Task {
|
||||||
|
let currentDate = Date()
|
||||||
|
print("[WidgetKit] [NotificationProvider] Getting timeline at \(currentDate)...")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let takeLimit: Int
|
||||||
|
switch context.family {
|
||||||
|
case .systemSmall:
|
||||||
|
takeLimit = 0
|
||||||
|
case .systemMedium:
|
||||||
|
takeLimit = 1
|
||||||
|
case .systemLarge:
|
||||||
|
takeLimit = 3
|
||||||
|
default:
|
||||||
|
takeLimit = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async let notifications = try await notificationService.fetchRecentNotifications(take: takeLimit)
|
||||||
|
async let unreadCount = try await notificationService.fetchUnreadCount()
|
||||||
|
|
||||||
|
let notifs = try await notifications
|
||||||
|
let unread = try await unreadCount
|
||||||
|
|
||||||
|
print("[WidgetKit] [NotificationProvider] Timeline - Notifications: \(notifs.count), Unread: \(unread)")
|
||||||
|
|
||||||
|
let entry = NotificationEntry(date: currentDate, notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
|
||||||
|
|
||||||
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
|
||||||
|
print("[WidgetKit] [NotificationProvider] Next update at: \(nextUpdate)")
|
||||||
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||||
|
completion(timeline)
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] [NotificationProvider] Error in getTimeline: \(error.localizedDescription)")
|
||||||
|
let entry = NotificationEntry(date: currentDate, notifications: nil, unreadCount: 0, error: error.localizedDescription, isLoading: false)
|
||||||
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
|
||||||
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||||
|
completion(timeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationWidgetEntryView: View {
|
||||||
|
var entry: NotificationProvider.Entry
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let notifications = entry.notifications, !notifications.isEmpty {
|
||||||
|
HasNotificationsView(notifications: notifications, unreadCount: entry.unreadCount)
|
||||||
|
} else if entry.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
} else if let error = entry.error {
|
||||||
|
ErrorView(error: error)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isCompact: Bool {
|
||||||
|
family == .systemSmall || isAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isAccessory: Bool {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
if case .accessoryRectangular = family {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func HasNotificationsView(notifications: [SnNotification], unreadCount: Int) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://notifications")!) {
|
||||||
|
if isCompact {
|
||||||
|
if isAccessory {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "bell.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.padding(.leading, 1.5)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if unreadCount > 0 {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text("\(unreadCount)")
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.blue.opacity(0.5))
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("unread", comment: "unread"))
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("on the Solar Network")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.horizontal, 1.5)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "bell.fill")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}.padding(.bottom, 8)
|
||||||
|
|
||||||
|
Spacer(minLength: 2)
|
||||||
|
|
||||||
|
if unreadCount > 0 {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("\(unreadCount)")
|
||||||
|
.font(.system(.largeTitle, design: .rounded))
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 2)
|
||||||
|
|
||||||
|
if unreadCount > 0 {
|
||||||
|
Text(NSLocalizedString("unreadNotifications", comment: "unread notifications"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} else {
|
||||||
|
Text(NSLocalizedString("noUnreadNotifications", comment: "no unread notifications"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "bell.fill")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if unreadCount > 0 {
|
||||||
|
Text("\(unreadCount)")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(.blue)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
}.padding(.bottom, 8)
|
||||||
|
|
||||||
|
let displayCount = family == .systemMedium ? 1 : 5
|
||||||
|
let displayNotifications = Array(notifications.prefix(displayCount))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
ForEach(displayNotifications) { notification in
|
||||||
|
NotificationItemView(notification: notification, compact: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if family == .systemMedium {
|
||||||
|
Spacer()
|
||||||
|
} else {
|
||||||
|
Spacer()
|
||||||
|
Text(NSLocalizedString("tapToViewAll", comment: "Tap to view all notifications"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func NotificationItemView(notification: SnNotification, compact: Bool) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: compact ? 6 : 12) {
|
||||||
|
if compact {
|
||||||
|
Image(systemName: notification.getTopicIcon())
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.2))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
|
||||||
|
Image(systemName: notification.getTopicIcon())
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(notification.title)
|
||||||
|
.font(compact ? .caption : .subheadline)
|
||||||
|
.fontWeight(notification.isUnread ? .semibold : .regular)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
if !compact && !notification.subtitle.isEmpty {
|
||||||
|
Text(notification.subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compact && !notification.content.isEmpty {
|
||||||
|
Text(notification.content)
|
||||||
|
.font(.caption2)
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let createdDate = notification.createdDate {
|
||||||
|
Text(formatRelativeTime(createdDate))
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if notification.isUnread {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.blue)
|
||||||
|
.frame(width: compact ? 6 : 8, height: compact ? 6 : 8)
|
||||||
|
.padding(.trailing, 6)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, compact ? 2 : 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func NotificationCompactItem(notification: SnNotification) -> some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: notification.getTopicIcon())
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(notification.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.lineLimit(1)
|
||||||
|
.fontWeight(notification.isUnread ? .semibold : .regular)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func EmptyView() -> some View {
|
||||||
|
Link(destination: URL(string: "solian://notifications")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "bell")
|
||||||
|
.font(isAccessory ? .caption : .title3)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
||||||
|
.font(isAccessory ? .caption2 : .headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Text(NSLocalizedString("noNotifications", comment: "No notifications yet"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 4 : 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func LoadingView() -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(isAccessory ? 0.6 : 0.8)
|
||||||
|
Text(NSLocalizedString("loading", comment: "Loading..."))
|
||||||
|
.font(isAccessory ? .caption2 : .caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 4 : 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func ErrorView(error: String) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://notifications")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(isAccessory ? .caption : .title3)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("error", comment: "Error"))
|
||||||
|
.font(isAccessory ? .caption2 : .headline)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAccessory {
|
||||||
|
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(error)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(isAccessory ? 4 : 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatRelativeTime(_ date: Date) -> String {
|
||||||
|
let now = Date()
|
||||||
|
let interval = now.timeIntervalSince(date)
|
||||||
|
|
||||||
|
if interval < 60 {
|
||||||
|
return NSLocalizedString("justNow", comment: "Just now")
|
||||||
|
} else if interval < 3600 {
|
||||||
|
let minutes = Int(interval / 60)
|
||||||
|
return String(format: NSLocalizedString("minutesAgo", comment: "%d min ago"), minutes)
|
||||||
|
} else if interval < 86400 {
|
||||||
|
let hours = Int(interval / 3600)
|
||||||
|
return String(format: NSLocalizedString("hoursAgo", comment: "%d hr ago"), hours)
|
||||||
|
} else {
|
||||||
|
let days = Int(interval / 86400)
|
||||||
|
return String(format: NSLocalizedString("daysAgo", comment: "%d d ago"), days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationWidgetRootView: View {
|
||||||
|
var entry: NotificationProvider.Entry
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
ZStack {
|
||||||
|
NotificationWidgetEntryView(entry: entry)
|
||||||
|
|
||||||
|
if let notifications = entry.notifications, !notifications.isEmpty {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(
|
||||||
|
width: geometry.size.width * 0.9,
|
||||||
|
height: geometry.size.width * 0.9
|
||||||
|
)
|
||||||
|
.opacity(0.12)
|
||||||
|
.mask(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [
|
||||||
|
Color.white,
|
||||||
|
Color.white,
|
||||||
|
Color.clear
|
||||||
|
]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.position(
|
||||||
|
x: geometry.size.width * 0.85,
|
||||||
|
y: 20
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.fill.tertiary, for: .widget)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
} else {
|
||||||
|
NotificationWidgetEntryView(entry: entry)
|
||||||
|
.padding()
|
||||||
|
.background()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SolianNotificationWidget: Widget {
|
||||||
|
let kind: String = "SolianNotificationWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: NotificationProvider()) { entry in
|
||||||
|
NotificationWidgetRootView(entry: entry)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Notifications")
|
||||||
|
.description("View your recent notifications")
|
||||||
|
.supportedFamilies(supportedFamilies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var supportedFamilies: [WidgetFamily] {
|
||||||
|
#if os(iOS)
|
||||||
|
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular]
|
||||||
|
#else
|
||||||
|
return [.systemSmall, .systemMedium, .systemLarge]
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .accessoryRectangular) {
|
||||||
|
SolianNotificationWidget()
|
||||||
|
} timeline: {
|
||||||
|
NotificationEntry(
|
||||||
|
date: .now,
|
||||||
|
notifications: [
|
||||||
|
SnNotification(
|
||||||
|
id: "1",
|
||||||
|
topic: "post.replies",
|
||||||
|
title: "New reply to your post",
|
||||||
|
subtitle: "Someone replied to your message",
|
||||||
|
content: "This is notification content",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "2",
|
||||||
|
topic: "relationships.friends.request",
|
||||||
|
title: "New friend request",
|
||||||
|
subtitle: "You have a pending friend request",
|
||||||
|
content: "Someone wants to be your friend",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
unreadCount: 1,
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemSmall) {
|
||||||
|
SolianNotificationWidget()
|
||||||
|
} timeline: {
|
||||||
|
NotificationEntry(
|
||||||
|
date: .now,
|
||||||
|
notifications: [
|
||||||
|
SnNotification(
|
||||||
|
id: "1",
|
||||||
|
topic: "post.replies",
|
||||||
|
title: "New reply to your post",
|
||||||
|
subtitle: "Someone replied to your message",
|
||||||
|
content: "This is notification content",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "2",
|
||||||
|
topic: "relationships.friends.request",
|
||||||
|
title: "New friend request",
|
||||||
|
subtitle: "You have a pending friend request",
|
||||||
|
content: "Someone wants to be your friend",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
unreadCount: 1,
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemMedium) {
|
||||||
|
SolianNotificationWidget()
|
||||||
|
} timeline: {
|
||||||
|
NotificationEntry(
|
||||||
|
date: .now,
|
||||||
|
notifications: [
|
||||||
|
SnNotification(
|
||||||
|
id: "1",
|
||||||
|
topic: "post.replies",
|
||||||
|
title: "New reply to your post",
|
||||||
|
subtitle: "Someone replied to your message",
|
||||||
|
content: "This is notification content",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "2",
|
||||||
|
topic: "relationships.friends.request",
|
||||||
|
title: "New friend request",
|
||||||
|
subtitle: "You have a pending friend request",
|
||||||
|
content: "Someone wants to be your friend",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "3",
|
||||||
|
topic: "invites.chat",
|
||||||
|
title: "New chat invite",
|
||||||
|
subtitle: "You've been invited to a chat",
|
||||||
|
content: "Join the conversation",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
unreadCount: 2,
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
#Preview(as: .systemLarge) {
|
||||||
|
SolianNotificationWidget()
|
||||||
|
} timeline: {
|
||||||
|
NotificationEntry(
|
||||||
|
date: .now,
|
||||||
|
notifications: [
|
||||||
|
SnNotification(
|
||||||
|
id: "1",
|
||||||
|
topic: "post.replies",
|
||||||
|
title: "New reply",
|
||||||
|
subtitle: "Someone replied",
|
||||||
|
content: "Content",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "2",
|
||||||
|
topic: "relationships.friends.request",
|
||||||
|
title: "New friend request",
|
||||||
|
subtitle: "You have a pending friend request",
|
||||||
|
content: "Someone wants to be your friend",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: nil,
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
),
|
||||||
|
SnNotification(
|
||||||
|
id: "3",
|
||||||
|
topic: "invites.chat",
|
||||||
|
title: "New chat invite",
|
||||||
|
subtitle: "You've been invited to a chat",
|
||||||
|
content: "Join the conversation",
|
||||||
|
meta: nil,
|
||||||
|
priority: 0,
|
||||||
|
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
||||||
|
accountId: "acc-1",
|
||||||
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
||||||
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
||||||
|
deletedAt: nil
|
||||||
|
)
|
||||||
|
],
|
||||||
|
unreadCount: 3,
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
17
ios/SolianWidgetExtension/SolianWidgetExtensionBundle.swift
Normal file
17
ios/SolianWidgetExtension/SolianWidgetExtensionBundle.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// SolianWidgetExtensionBundle.swift
|
||||||
|
// SolianWidgetExtension
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2026/1/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct SolianWidgetExtensionBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
SolianCheckInWidget()
|
||||||
|
SolianNotificationWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
136
ios/SolianWidgetExtension/WidgetNetworking.swift
Normal file
136
ios/SolianWidgetExtension/WidgetNetworking.swift
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
//
|
||||||
|
// Networking.swift
|
||||||
|
// SolianWidgetExtensionExtension
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2026/1/4.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum RemoteError: Error {
|
||||||
|
case missingCredentials
|
||||||
|
case invalidURL
|
||||||
|
case invalidResponse
|
||||||
|
case httpError(Int)
|
||||||
|
case decodingError
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RemoteError: LocalizedError {
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .missingCredentials:
|
||||||
|
return "Please open the app to sign in."
|
||||||
|
case .invalidURL:
|
||||||
|
return "Invalid server configuration."
|
||||||
|
case .invalidResponse:
|
||||||
|
return "Server returned an invalid response."
|
||||||
|
case .httpError(let code):
|
||||||
|
return "Server error (\(code))."
|
||||||
|
case .decodingError:
|
||||||
|
return "Failed to read server data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TokenData: Codable {
|
||||||
|
let token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetNetworkService {
|
||||||
|
private let appGroup = "group.solsynth.solian"
|
||||||
|
private let tokenKey = "flutter.dyn_user_tk"
|
||||||
|
private let urlKey = "flutter.app_server_url"
|
||||||
|
|
||||||
|
private lazy var session: URLSession = {
|
||||||
|
let configuration = URLSessionConfiguration.ephemeral
|
||||||
|
configuration.timeoutIntervalForRequest = 10.0
|
||||||
|
configuration.timeoutIntervalForResource = 10.0
|
||||||
|
configuration.waitsForConnectivity = false
|
||||||
|
return URLSession(configuration: configuration)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var userDefaults: UserDefaults? {
|
||||||
|
UserDefaults(suiteName: appGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
var token: String? {
|
||||||
|
guard let tokenString = userDefaults?.string(forKey: tokenKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = tokenString.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let tokenData = try JSONDecoder().decode(TokenData.self, from: data)
|
||||||
|
return tokenData.token
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] Failed to decode token: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseURL: String {
|
||||||
|
return userDefaults?.string(forKey: urlKey) ?? "https://api.solian.app"
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRequest<T: Codable>(
|
||||||
|
path: String,
|
||||||
|
method: String = "GET",
|
||||||
|
headers: [String: String] = [:]
|
||||||
|
) async throws -> T? {
|
||||||
|
guard let token = token else {
|
||||||
|
throw RemoteError.missingCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = URL(string: "\(baseURL)\(path)") else {
|
||||||
|
throw RemoteError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = method
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
for (key, value) in headers {
|
||||||
|
request.setValue(value, forHTTPHeaderField: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.timeoutInterval = 10.0
|
||||||
|
|
||||||
|
print("[WidgetKit] [Network] Requesting: \(baseURL)\(path)")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw RemoteError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[WidgetKit] [Network] Status: \(httpResponse.statusCode), Data length: \(data.count)")
|
||||||
|
|
||||||
|
if let jsonString = String(data: data, encoding: .utf8) {
|
||||||
|
print("[WidgetKit] [Network] Response: \(jsonString.prefix(500))")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpResponse.statusCode {
|
||||||
|
case 200...299:
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
do {
|
||||||
|
let result = try decoder.decode(T.self, from: data)
|
||||||
|
print("[WidgetKit] [Network] Successfully decoded response")
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
print("[WidgetKit] [Network] Decoding error: \(error.localizedDescription)")
|
||||||
|
print("[WidgetKit] [Network] Expected type: \(String(describing: T.self))")
|
||||||
|
throw RemoteError.decodingError
|
||||||
|
}
|
||||||
|
case 404:
|
||||||
|
print("[WidgetKit] [Network] Resource not found (404)")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
print("[WidgetKit] [Network] HTTP Error: \(httpResponse.statusCode)")
|
||||||
|
throw RemoteError.httpError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
ios/SolianWidgetExtension/en.lproj/Localizable.strings
Normal file
41
ios/SolianWidgetExtension/en.lproj/Localizable.strings
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "Great Misfortune";
|
||||||
|
"checkInResultT1" = "Misfortune";
|
||||||
|
"checkInResultT2" = "Moderate";
|
||||||
|
"checkInResultT3" = "Fortune";
|
||||||
|
"checkInResultT4" = "Great Fortune";
|
||||||
|
"checkInResultT5" = "Special";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "Check In";
|
||||||
|
"tapToCheckIn" = "Tap to check in today";
|
||||||
|
"error" = "Error";
|
||||||
|
"openAppToRefresh" = "Open app to refresh";
|
||||||
|
"loading" = "Loading...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d EXP";
|
||||||
|
"footer" = "Solian Check In";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@ is today!";
|
||||||
|
"notableDayIs" = "%@ is %@";
|
||||||
|
"notableDayUpcoming" = "Upcoming";
|
||||||
|
"today" = "Today";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "Notifications";
|
||||||
|
"noNotifications" = "No notifications yet";
|
||||||
|
"tapToViewAll" = "Tap to view all notifications";
|
||||||
|
"justNow" = "Just now";
|
||||||
|
"minutesAgo" = "%d min ago";
|
||||||
|
"hoursAgo" = "%d hr ago";
|
||||||
|
"daysAgo" = "%d d ago";
|
||||||
|
"pleaseSignIn" = "Please open the app to sign in.";
|
||||||
|
"invalidServerConfig" = "Invalid server configuration.";
|
||||||
|
"invalidResponse" = "Server returned an invalid response.";
|
||||||
|
"httpError" = "Server error (%d).";
|
||||||
|
"decodingError" = "Failed to read server data.";
|
||||||
|
"unreadNotifications" = "Notification(s) unread";
|
||||||
|
"noNotifications" = "No notifications";
|
||||||
|
"noUnreadNotifications" = "All notifications are read";
|
||||||
|
"unread" = "unread";
|
||||||
36
ios/SolianWidgetExtension/es.lproj/Localizable.strings
Normal file
36
ios/SolianWidgetExtension/es.lproj/Localizable.strings
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "Gran Desventura";
|
||||||
|
"checkInResultT1" = "Desventura";
|
||||||
|
"checkInResultT2" = "Moderado";
|
||||||
|
"checkInResultT3" = "Buena Fortuna";
|
||||||
|
"checkInResultT4" = "Gran Fortuna";
|
||||||
|
"checkInResultT5" = "Especial";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "Registrar";
|
||||||
|
"tapToCheckIn" = "Toca para registrar hoy";
|
||||||
|
"error" = "Error";
|
||||||
|
"openAppToRefresh" = "Abre la aplicación para actualizar";
|
||||||
|
"loading" = "Cargando...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d EXP";
|
||||||
|
"footer" = "Registro Solian";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@ es hoy!";
|
||||||
|
"notableDayIs" = "%@ es %@";
|
||||||
|
"today" = "Hoy";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "Notificaciones";
|
||||||
|
"noNotifications" = "Aún no hay notificaciones";
|
||||||
|
"tapToViewAll" = "Toca para ver todas las notificaciones";
|
||||||
|
"justNow" = "Ahora mismo";
|
||||||
|
"minutesAgo" = "hace %d min";
|
||||||
|
"hoursAgo" = "hace %d hr";
|
||||||
|
"daysAgo" = "hace %d días";
|
||||||
|
"pleaseSignIn" = "Por favor, abre la aplicación para iniciar sesión.";
|
||||||
|
"invalidServerConfig" = "Configuración del servidor no válida.";
|
||||||
|
"invalidResponse" = "El servidor devolvió una respuesta no válida.";
|
||||||
|
"httpError" = "Error del servidor (%d).";
|
||||||
|
"decodingError" = "Error al leer los datos del servidor.";
|
||||||
36
ios/SolianWidgetExtension/ja.lproj/Localizable.strings
Normal file
36
ios/SolianWidgetExtension/ja.lproj/Localizable.strings
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "大凶";
|
||||||
|
"checkInResultT1" = "凶";
|
||||||
|
"checkInResultT2" = "中平";
|
||||||
|
"checkInResultT3" = "吉";
|
||||||
|
"checkInResultT4" = "大吉";
|
||||||
|
"checkInResultT5" = "特殊";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "チェックイン";
|
||||||
|
"tapToCheckIn" = "タップして今日チェックイン";
|
||||||
|
"error" = "エラー";
|
||||||
|
"openAppToRefresh" = "アプリを開いて更新";
|
||||||
|
"loading" = "読み込み中...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d 経験値";
|
||||||
|
"footer" = "Solian チェックイン";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@は今日です!";
|
||||||
|
"notableDayIs" = "%@は%@です";
|
||||||
|
"today" = "今日";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "通知";
|
||||||
|
"noNotifications" = "まだ通知はありません";
|
||||||
|
"tapToViewAll" = "タップしてすべての通知を表示";
|
||||||
|
"justNow" = "たった今";
|
||||||
|
"minutesAgo" = "%d分前";
|
||||||
|
"hoursAgo" = "%d時間前";
|
||||||
|
"daysAgo" = "%d日前";
|
||||||
|
"pleaseSignIn" = "アプリを開いてサインインしてください。";
|
||||||
|
"invalidServerConfig" = "サーバー設定が無効です。";
|
||||||
|
"invalidResponse" = "サーバーから無効な応答が返されました。";
|
||||||
|
"httpError" = "サーバーエラー (%d)。";
|
||||||
|
"decodingError" = "サーバーデータの読み込みに失敗しました。";
|
||||||
36
ios/SolianWidgetExtension/ko.lproj/Localizable.strings
Normal file
36
ios/SolianWidgetExtension/ko.lproj/Localizable.strings
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "대흉";
|
||||||
|
"checkInResultT1" = "흉";
|
||||||
|
"checkInResultT2" = "중평";
|
||||||
|
"checkInResultT3" = "길";
|
||||||
|
"checkInResultT4" = "대길";
|
||||||
|
"checkInResultT5" = "특수";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "체크인";
|
||||||
|
"tapToCheckIn" = "탭하여 오늘 체크인";
|
||||||
|
"error" = "오류";
|
||||||
|
"openAppToRefresh" = "앱을 열어 새로고침";
|
||||||
|
"loading" = "로딩 중...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d 경험치";
|
||||||
|
"footer" = "Solian 체크인";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@ 오늘입니다!";
|
||||||
|
"notableDayIs" = "%@ 은/는 %@";
|
||||||
|
"today" = "오늘";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "알림";
|
||||||
|
"noNotifications" = "아직 알림이 없습니다";
|
||||||
|
"tapToViewAll" = "탭하여 모든 알림 보기";
|
||||||
|
"justNow" = "방금";
|
||||||
|
"minutesAgo" = "%d분 전";
|
||||||
|
"hoursAgo" = "%d시간 전";
|
||||||
|
"daysAgo" = "%d일 전";
|
||||||
|
"pleaseSignIn" = "앱을 열어 로그인하세요.";
|
||||||
|
"invalidServerConfig" = "서버 구성이 올바르지 않습니다.";
|
||||||
|
"invalidResponse" = "서버에서 잘못된 응답을 받았습니다.";
|
||||||
|
"httpError" = "서버 오류 (%d).";
|
||||||
|
"decodingError" = "서버 데이터 읽기에 실패했습니다.";
|
||||||
41
ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings
Normal file
41
ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "大凶";
|
||||||
|
"checkInResultT1" = "凶";
|
||||||
|
"checkInResultT2" = "中平";
|
||||||
|
"checkInResultT3" = "吉";
|
||||||
|
"checkInResultT4" = "大吉";
|
||||||
|
"checkInResultT5" = "特殊";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "打卡";
|
||||||
|
"tapToCheckIn" = "点击今日打卡";
|
||||||
|
"error" = "错误";
|
||||||
|
"openAppToRefresh" = "打开应用以刷新";
|
||||||
|
"loading" = "加载中...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d 经验值";
|
||||||
|
"footer" = "Solian 签到";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@是今天!";
|
||||||
|
"notableDayIs" = "%@ 是 %@";
|
||||||
|
"notableDayUpcoming" = "接下来";
|
||||||
|
"today" = "今天";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "通知";
|
||||||
|
"noNotifications" = "还没有通知";
|
||||||
|
"tapToViewAll" = "点击查看所有通知";
|
||||||
|
"justNow" = "刚刚";
|
||||||
|
"minutesAgo" = "%d分钟前";
|
||||||
|
"hoursAgo" = "%d小时前";
|
||||||
|
"daysAgo" = "%d天前";
|
||||||
|
"pleaseSignIn" = "请打开应用登录。";
|
||||||
|
"invalidServerConfig" = "服务器配置无效。";
|
||||||
|
"invalidResponse" = "服务器返回了无效响应。";
|
||||||
|
"httpError" = "服务器错误 (%d)。";
|
||||||
|
"decodingError" = "读取服务器数据失败。";
|
||||||
|
"unreadNotifications" = "条通知未读";
|
||||||
|
"noNotifications" = "没有通知";
|
||||||
|
"noUnreadNotifications" = "没有未读通知";
|
||||||
|
"unread" = "未读";
|
||||||
36
ios/SolianWidgetExtension/zh-Hant.lproj/Localizable.strings
Normal file
36
ios/SolianWidgetExtension/zh-Hant.lproj/Localizable.strings
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "大凶";
|
||||||
|
"checkInResultT1" = "凶";
|
||||||
|
"checkInResultT2" = "中平";
|
||||||
|
"checkInResultT3" = "吉";
|
||||||
|
"checkInResultT4" = "大吉";
|
||||||
|
"checkInResultT5" = "特殊";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "打卡";
|
||||||
|
"tapToCheckIn" = "點擊今日打卡";
|
||||||
|
"error" = "錯誤";
|
||||||
|
"openAppToRefresh" = "打開應用以刷新";
|
||||||
|
"loading" = "載入中...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d 經驗值";
|
||||||
|
"footer" = "Solian 簽到";
|
||||||
|
|
||||||
|
/* Notable Day Strings */
|
||||||
|
"notableDayToday" = "%@是今天!";
|
||||||
|
"notableDayIs" = "%@ 是 %@";
|
||||||
|
"today" = "今天";
|
||||||
|
|
||||||
|
/* Notification Widget Strings */
|
||||||
|
"notifications" = "通知";
|
||||||
|
"noNotifications" = "還沒有通知";
|
||||||
|
"tapToViewAll" = "點擊查看所有通知";
|
||||||
|
"justNow" = "剛剛";
|
||||||
|
"minutesAgo" = "%d分鐘前";
|
||||||
|
"hoursAgo" = "%d小時前";
|
||||||
|
"daysAgo" = "%d天前";
|
||||||
|
"pleaseSignIn" = "請打開應用登錄。";
|
||||||
|
"invalidServerConfig" = "伺服器配置無效。";
|
||||||
|
"invalidResponse" = "伺服器返回了無效響應。";
|
||||||
|
"httpError" = "伺服器錯誤 (%d)。";
|
||||||
|
"decodingError" = "讀取伺服器數據失敗。";
|
||||||
12
ios/SolianWidgetExtensionExtension.entitlements
Normal file
12
ios/SolianWidgetExtensionExtension.entitlements
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.solsynth.solian</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -20,6 +20,7 @@ import 'package:island/pods/userinfo.dart';
|
|||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/services/notify.dart';
|
import 'package:island/services/notify.dart';
|
||||||
|
import 'package:island/services/widget_sync_service.dart';
|
||||||
import 'package:island/services/timezone.dart';
|
import 'package:island/services/timezone.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
@@ -282,6 +283,11 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
ref.listen(websocketStateProvider, (_, state) {
|
ref.listen(websocketStateProvider, (_, state) {
|
||||||
talker.info('[WebSocket] $state');
|
talker.info('[WebSocket] $state');
|
||||||
});
|
});
|
||||||
|
ref.listen(userInfoProvider, (_, user) {
|
||||||
|
if (user.value != null) {
|
||||||
|
WidgetSyncService().syncToWidget();
|
||||||
|
}
|
||||||
|
});
|
||||||
Future(() {
|
Future(() {
|
||||||
userNotifier.fetchUser().then((_) {
|
userNotifier.fetchUser().then((_) {
|
||||||
final user = ref.watch(userInfoProvider);
|
final user = ref.watch(userInfoProvider);
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ final class SubscribedFeedsProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$subscribedFeedsHash() => r'5c0c8c30c5f543f6ea1d39786a6778f77ba5b3df';
|
String _$subscribedFeedsHash() => r'91d6f909a3d2c9f68028550223f7c7b2128727d2';
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ final class MarketplaceWebFeedProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$marketplaceWebFeedHash() =>
|
String _$marketplaceWebFeedHash() =>
|
||||||
r'8383f94f1bc272b903c341b8d95000313b69d14c';
|
r'36f3235ba346b0d416ce5e66dca8d6cecbafb608';
|
||||||
|
|
||||||
final class MarketplaceWebFeedFamily extends $Family
|
final class MarketplaceWebFeedFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<FutureOr<SnWebFeed>, String> {
|
with $FunctionalFamilyOverride<FutureOr<SnWebFeed>, String> {
|
||||||
@@ -141,7 +141,7 @@ final class MarketplaceWebFeedSubscriptionProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$marketplaceWebFeedSubscriptionHash() =>
|
String _$marketplaceWebFeedSubscriptionHash() =>
|
||||||
r'2ff06a48ed7d4236b57412ecca55e94c0a0b6330';
|
r'6efa43b4d7e2ab62a721a67e035038dcf63be524';
|
||||||
|
|
||||||
/// Provider for web feed subscription status
|
/// Provider for web feed subscription status
|
||||||
|
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ class PostDetailScreen extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: 600),
|
constraints: BoxConstraints(maxWidth: 800),
|
||||||
child: PostActionButtons(
|
child: PostActionButtons(
|
||||||
post: post,
|
post: post,
|
||||||
renderingPadding: const EdgeInsets.symmetric(
|
renderingPadding: const EdgeInsets.symmetric(
|
||||||
@@ -506,7 +506,7 @@ class PostDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PostRepliesList(postId: id, maxWidth: 600),
|
PostRepliesList(postId: id, maxWidth: 800),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 80),
|
SliverGap(MediaQuery.of(context).padding.bottom + 80),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
24
lib/services/widget_sync_service.dart
Normal file
24
lib/services/widget_sync_service.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class WidgetSyncService {
|
||||||
|
static const _channel = MethodChannel('dev.solsynth.solian/widget');
|
||||||
|
static final _instance = WidgetSyncService._internal();
|
||||||
|
|
||||||
|
factory WidgetSyncService() => _instance;
|
||||||
|
|
||||||
|
WidgetSyncService._internal();
|
||||||
|
|
||||||
|
bool get _isSupported => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||||
|
|
||||||
|
Future<void> syncToWidget() async {
|
||||||
|
if (!_isSupported) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod('syncToWidget');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to sync to widget: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -269,7 +269,17 @@ class AppWrapper extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path == '/notifications') {
|
||||||
|
eventBus.fire(ShowNotificationSheetEvent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final router = ref.read(routerProvider);
|
final router = ref.read(routerProvider);
|
||||||
|
if (path == '/dashboard') {
|
||||||
|
router.go('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path = Uri.parse(
|
path = Uri.parse(
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/models/embed.dart';
|
import 'package:island/models/embed.dart';
|
||||||
import 'package:island/utils/mapping.dart';
|
|
||||||
import 'package:island/widgets/content/embed/link.dart';
|
import 'package:island/widgets/content/embed/link.dart';
|
||||||
import 'package:island/widgets/poll/poll_submit.dart';
|
import 'package:island/widgets/poll/poll_submit.dart';
|
||||||
import 'package:island/widgets/wallet/fund_envelope.dart';
|
import 'package:island/widgets/wallet/fund_envelope.dart';
|
||||||
@@ -25,15 +24,8 @@ class EmbedListWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final normalizedEmbeds = embeds
|
final linkEmbeds = embeds.where((e) => e['type'] == 'link').toList();
|
||||||
.map((e) => convertMapKeysToSnakeCase(e as Map<String, dynamic>))
|
final otherEmbeds = embeds.where((e) => e['type'] != 'link').toList();
|
||||||
.toList();
|
|
||||||
final linkEmbeds = normalizedEmbeds
|
|
||||||
.where((e) => e['type'] == 'link')
|
|
||||||
.toList();
|
|
||||||
final otherEmbeds = normalizedEmbeds
|
|
||||||
.where((e) => e['type'] != 'link')
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
|
|||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
title: Text(publisher.nick),
|
title: Text(publisher.nick),
|
||||||
subtitle: Text('@${publisher.name}'),
|
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
@@ -146,9 +145,11 @@ class PostSubscriptionFilterWidget extends HookConsumerWidget {
|
|||||||
dense: true,
|
dense: true,
|
||||||
secondary: ProfilePictureWidget(
|
secondary: ProfilePictureWidget(
|
||||||
file: subscription.publisher.picture,
|
file: subscription.publisher.picture,
|
||||||
|
radius: 12,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.only(
|
||||||
horizontal: 16,
|
left: 15,
|
||||||
|
right: 16,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.5.0+159
|
version: 3.5.0+160
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.0
|
sdk: ^3.8.0
|
||||||
|
|||||||
Reference in New Issue
Block a user