✨ iOS check in widget
This commit is contained in:
@@ -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": "已添加",
|
||||||
|
|||||||
@@ -467,8 +467,6 @@
|
|||||||
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
|
7301DB062F08D99C008390F3 /* SolianWidgetExtension */,
|
||||||
);
|
);
|
||||||
name = SolianWidgetExtensionExtension;
|
name = SolianWidgetExtensionExtension;
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = SolianWidgetExtensionExtension;
|
productName = SolianWidgetExtensionExtension;
|
||||||
productReference = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */;
|
productReference = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */;
|
||||||
productType = "com.apple.product-type.app-extension";
|
productType = "com.apple.product-type.app-extension";
|
||||||
@@ -632,6 +630,7 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
"zh-Hans",
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
@@ -1203,7 +1202,7 @@
|
|||||||
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1249,7 +1248,7 @@
|
|||||||
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -1292,7 +1291,7 @@
|
|||||||
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
INFOPLIST_FILE = SolianWidgetExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = SolianWidgetExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -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,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 = "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
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import WatchConnectivity
|
|||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
syncDefaultsToGroup()
|
syncDefaultsToGroup()
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
@@ -31,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
|
||||||
@@ -40,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 {
|
||||||
|
|||||||
@@ -10,26 +10,32 @@ import Foundation
|
|||||||
private let flutterKeyPrefix = "flutter."
|
private let flutterKeyPrefix = "flutter."
|
||||||
|
|
||||||
private let flutterKeysToSync: [String] = [
|
private let flutterKeysToSync: [String] = [
|
||||||
"dyn_user_tk"
|
"dyn_user_tk",
|
||||||
|
"app_server_url"
|
||||||
]
|
]
|
||||||
|
|
||||||
func syncDefaultsToGroup() {
|
func syncDefaultsToGroup() {
|
||||||
|
print("[iOS] syncDefaultsToGroup() called")
|
||||||
|
|
||||||
let standard = UserDefaults.standard
|
let standard = UserDefaults.standard
|
||||||
let shared = UserDefaults(suiteName: "dev.solsynth.solian")
|
let shared = UserDefaults(suiteName: "group.solsynth.solian")
|
||||||
|
|
||||||
guard let shared else {
|
guard let shared else {
|
||||||
print("[iOS] App Group UserDefaults not available")
|
print("[iOS] App Group UserDefaults not available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in flutterKeysToSync {
|
for key in flutterKeysToSync {
|
||||||
guard key.hasPrefix(flutterKeyPrefix) else { continue }
|
let prefixedKey = key.starts(with: flutterKeyPrefix) ? key : flutterKeyPrefix + key
|
||||||
|
|
||||||
if let value = standard.object(forKey: key) {
|
if let value = standard.object(forKey: prefixedKey) {
|
||||||
print("[iOS] Syncing key to App Group: \(key)")
|
print("[iOS] Syncing key to App Group: \(prefixedKey)")
|
||||||
shared.set(value, forKey: key)
|
shared.set(value, forKey: prefixedKey)
|
||||||
|
} else {
|
||||||
|
print("[iOS] Key \(prefixedKey) was not found in the app data, skipping...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shared.synchronize()
|
shared.synchronize()
|
||||||
|
print("[iOS] Sync completed")
|
||||||
}
|
}
|
||||||
|
|||||||
16
ios/SolianWidgetExtension/Base.lproj/Localizable.strings
Normal file
16
ios/SolianWidgetExtension/Base.lproj/Localizable.strings
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "Worst";
|
||||||
|
"checkInResultT1" = "Poor";
|
||||||
|
"checkInResultT2" = "Mid";
|
||||||
|
"checkInResultT3" = "Good";
|
||||||
|
"checkInResultT4" = "Best";
|
||||||
|
"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 XP";
|
||||||
@@ -8,51 +8,483 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
return try decoder.decode(T.self, from: data)
|
||||||
|
case 404:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw RemoteError.httpError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckInService {
|
||||||
|
private let networkService = WidgetNetworkService()
|
||||||
|
|
||||||
|
func fetchCheckInResult() async throws -> CheckInResult? {
|
||||||
|
return try await networkService.makeRequest(path: "/pass/accounts/me/check-in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CheckInEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let result: CheckInResult?
|
||||||
|
let error: String?
|
||||||
|
let isLoading: Bool
|
||||||
|
|
||||||
|
static func placeholder() -> CheckInEntry {
|
||||||
|
CheckInEntry(date: Date(), result: nil, error: nil, isLoading: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Provider: TimelineProvider {
|
struct Provider: TimelineProvider {
|
||||||
func placeholder(in context: Context) -> SimpleEntry {
|
private let apiService = CheckInService()
|
||||||
SimpleEntry(date: Date(), emoji: "😀")
|
|
||||||
|
func placeholder(in context: Context) -> CheckInEntry {
|
||||||
|
CheckInEntry.placeholder()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
Task {
|
||||||
completion(entry)
|
let result = try? await apiService.fetchCheckInResult()
|
||||||
|
let entry = CheckInEntry(date: Date(), result: result, error: nil, isLoading: false)
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||||
var entries: [SimpleEntry] = []
|
Task {
|
||||||
|
let currentDate = Date()
|
||||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
|
||||||
let currentDate = Date()
|
do {
|
||||||
for hourOffset in 0 ..< 5 {
|
let result = try await apiService.fetchCheckInResult()
|
||||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
let entry = CheckInEntry(date: currentDate, result: result, error: nil, isLoading: false)
|
||||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
|
||||||
entries.append(entry)
|
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)!
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate))
|
||||||
|
completion(timeline)
|
||||||
|
} catch {
|
||||||
|
let entry = CheckInEntry(date: currentDate, result: 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
|
||||||
completion(timeline)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func relevances() async -> WidgetRelevances<Void> {
|
|
||||||
// // Generate a list containing the contexts this widget is relevant in.
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SimpleEntry: TimelineEntry {
|
struct CheckInWidgetEntryView: View {
|
||||||
let date: Date
|
|
||||||
let emoji: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SolianWidgetExtensionEntryView : View {
|
|
||||||
var entry: Provider.Entry
|
var entry: Provider.Entry
|
||||||
|
@Environment(\.widgetFamily) var family
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
if let result = entry.result {
|
||||||
Text("Time:")
|
CheckedInView(result: result)
|
||||||
Text(entry.date, style: .time)
|
} else if entry.isLoading {
|
||||||
|
LoadingView()
|
||||||
Text("Emoji:")
|
} else if let error = entry.error {
|
||||||
Text(entry.emoji)
|
ErrorView(error: error)
|
||||||
|
} else {
|
||||||
|
NotCheckedInView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getLevelName(for level: Int) -> String {
|
||||||
|
let key = "checkInResultT\(level)"
|
||||||
|
return NSLocalizedString(key, comment: "Check-in result level name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func CheckedInView(result: CheckInResult) -> some View {
|
||||||
|
Link(destination: URL(string: "solian://dashboard")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame.fill")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.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 }
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
if let positiveTip = positiveTips.first {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
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: 2) {
|
||||||
|
Image(systemName: "hand.thumbsdown.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(negativeTip.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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(.secondary)
|
||||||
|
Text(positiveTip.title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let negativeTip = negativeTips.first {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "hand.thumbsdown.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
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(.secondary)
|
||||||
|
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(.secondary)
|
||||||
|
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 !isAccessory {
|
||||||
|
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() -> some View {
|
||||||
|
Link(destination: URL(string: "solian://dashboard")!) {
|
||||||
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "flame")
|
||||||
|
.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)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
WidgetFooter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,22 +495,83 @@ struct SolianWidgetExtension: Widget {
|
|||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
SolianWidgetExtensionEntryView(entry: entry)
|
CheckInWidgetEntryView(entry: entry)
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
.containerBackground(.fill.tertiary, for: .widget)
|
||||||
} else {
|
} else {
|
||||||
SolianWidgetExtensionEntryView(entry: entry)
|
CheckInWidgetEntryView(entry: entry)
|
||||||
.padding()
|
.padding()
|
||||||
.background()
|
.background()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.configurationDisplayName("My Widget")
|
.configurationDisplayName("Check In")
|
||||||
.description("This is an example widget.")
|
.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) {
|
#Preview(as: .systemSmall) {
|
||||||
SolianWidgetExtension()
|
SolianWidgetExtension()
|
||||||
} timeline: {
|
} timeline: {
|
||||||
SimpleEntry(date: .now, emoji: "😀")
|
CheckInEntry(date: .now, result: nil, error: nil, isLoading: false)
|
||||||
SimpleEntry(date: .now, emoji: "🤩")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemMedium) {
|
||||||
|
SolianWidgetExtension()
|
||||||
|
} 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
|
||||||
|
),
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
#Preview(as: .accessoryRectangular) {
|
||||||
|
SolianWidgetExtension()
|
||||||
|
} 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
|
||||||
|
),
|
||||||
|
error: nil,
|
||||||
|
isLoading: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
16
ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings
Normal file
16
ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Check In Level Names */
|
||||||
|
"checkInResultT0" = "大凶";
|
||||||
|
"checkInResultT1" = "凶";
|
||||||
|
"checkInResultT2" = "中平";
|
||||||
|
"checkInResultT3" = "吉";
|
||||||
|
"checkInResultT4" = "大吉";
|
||||||
|
"checkInResultT5" = "特殊";
|
||||||
|
|
||||||
|
/* Widget UI Strings */
|
||||||
|
"checkIn" = "打卡";
|
||||||
|
"tapToCheckIn" = "点击今日打卡";
|
||||||
|
"error" = "错误";
|
||||||
|
"openAppToRefresh" = "打开应用以刷新";
|
||||||
|
"loading" = "加载中...";
|
||||||
|
"rewardPoints" = "%d";
|
||||||
|
"rewardExperience" = "%d 经验值";
|
||||||
@@ -6,5 +6,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.solsynth.solian</string>
|
<string>group.solsynth.solian</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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);
|
||||||
|
|||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -272,7 +272,7 @@ class AppWrapper extends HookConsumerWidget {
|
|||||||
final router = ref.read(routerProvider);
|
final router = ref.read(routerProvider);
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path = Uri.parse(
|
path = Uri.parse(
|
||||||
path,
|
path == '/dashboard' ? '/' : path,
|
||||||
).replace(queryParameters: uri.queryParameters).toString();
|
).replace(queryParameters: uri.queryParameters).toString();
|
||||||
}
|
}
|
||||||
router.push(path);
|
router.push(path);
|
||||||
|
|||||||
Reference in New Issue
Block a user