Showing
30 changed files
with
1231 additions
and
728 deletions
5.73 KB
9.28 KB
10.9 KB
40.4 KB
9.96 KB
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner Watch App (Notification).xcscheme
deleted
100644 → 0
| 1 | -<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | -<Scheme | ||
| 3 | - LastUpgradeVersion = "2600" | ||
| 4 | - version = "2.0"> | ||
| 5 | - <BuildAction | ||
| 6 | - parallelizeBuildables = "YES" | ||
| 7 | - buildImplicitDependencies = "YES" | ||
| 8 | - buildArchitectures = "Automatic"> | ||
| 9 | - <BuildActionEntries> | ||
| 10 | - <BuildActionEntry | ||
| 11 | - buildForTesting = "YES" | ||
| 12 | - buildForRunning = "YES" | ||
| 13 | - buildForProfiling = "YES" | ||
| 14 | - buildForArchiving = "YES" | ||
| 15 | - buildForAnalyzing = "YES"> | ||
| 16 | - <BuildableReference | ||
| 17 | - BuildableIdentifier = "primary" | ||
| 18 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 19 | - BuildableName = "Runner Watch App.app" | ||
| 20 | - BlueprintName = "Runner Watch App" | ||
| 21 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 22 | - </BuildableReference> | ||
| 23 | - </BuildActionEntry> | ||
| 24 | - <BuildActionEntry | ||
| 25 | - buildForTesting = "YES" | ||
| 26 | - buildForRunning = "YES" | ||
| 27 | - buildForProfiling = "YES" | ||
| 28 | - buildForArchiving = "YES" | ||
| 29 | - buildForAnalyzing = "YES"> | ||
| 30 | - <BuildableReference | ||
| 31 | - BuildableIdentifier = "primary" | ||
| 32 | - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4" | ||
| 33 | - BuildableName = "Runner.app" | ||
| 34 | - BlueprintName = "Runner" | ||
| 35 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 36 | - </BuildableReference> | ||
| 37 | - </BuildActionEntry> | ||
| 38 | - </BuildActionEntries> | ||
| 39 | - </BuildAction> | ||
| 40 | - <TestAction | ||
| 41 | - buildConfiguration = "Debug" | ||
| 42 | - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
| 43 | - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
| 44 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 45 | - shouldAutocreateTestPlan = "YES"> | ||
| 46 | - </TestAction> | ||
| 47 | - <LaunchAction | ||
| 48 | - buildConfiguration = "Debug" | ||
| 49 | - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
| 50 | - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
| 51 | - launchStyle = "0" | ||
| 52 | - useCustomWorkingDirectory = "NO" | ||
| 53 | - ignoresPersistentStateOnLaunch = "NO" | ||
| 54 | - debugDocumentVersioning = "YES" | ||
| 55 | - debugServiceExtension = "internal" | ||
| 56 | - allowLocationSimulation = "YES" | ||
| 57 | - launchAutomaticallySubstyle = "8"> | ||
| 58 | - <BuildableProductRunnable | ||
| 59 | - runnableDebuggingMode = "0"> | ||
| 60 | - <BuildableReference | ||
| 61 | - BuildableIdentifier = "primary" | ||
| 62 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 63 | - BuildableName = "Runner Watch App.app" | ||
| 64 | - BlueprintName = "Runner Watch App" | ||
| 65 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 66 | - </BuildableReference> | ||
| 67 | - </BuildableProductRunnable> | ||
| 68 | - </LaunchAction> | ||
| 69 | - <ProfileAction | ||
| 70 | - buildConfiguration = "Release" | ||
| 71 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 72 | - savedToolIdentifier = "" | ||
| 73 | - useCustomWorkingDirectory = "NO" | ||
| 74 | - debugDocumentVersioning = "YES" | ||
| 75 | - launchAutomaticallySubstyle = "8"> | ||
| 76 | - <BuildableProductRunnable | ||
| 77 | - runnableDebuggingMode = "0"> | ||
| 78 | - <BuildableReference | ||
| 79 | - BuildableIdentifier = "primary" | ||
| 80 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 81 | - BuildableName = "Runner Watch App.app" | ||
| 82 | - BlueprintName = "Runner Watch App" | ||
| 83 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 84 | - </BuildableReference> | ||
| 85 | - </BuildableProductRunnable> | ||
| 86 | - </ProfileAction> | ||
| 87 | - <AnalyzeAction | ||
| 88 | - buildConfiguration = "Debug"> | ||
| 89 | - </AnalyzeAction> | ||
| 90 | - <ArchiveAction | ||
| 91 | - buildConfiguration = "Release" | ||
| 92 | - revealArchiveInOrganizer = "YES"> | ||
| 93 | - </ArchiveAction> | ||
| 94 | -</Scheme> |
| 1 | -<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | -<Scheme | ||
| 3 | - LastUpgradeVersion = "2600" | ||
| 4 | - version = "1.7"> | ||
| 5 | - <BuildAction | ||
| 6 | - parallelizeBuildables = "YES" | ||
| 7 | - buildImplicitDependencies = "YES" | ||
| 8 | - buildArchitectures = "Automatic"> | ||
| 9 | - <BuildActionEntries> | ||
| 10 | - <BuildActionEntry | ||
| 11 | - buildForTesting = "YES" | ||
| 12 | - buildForRunning = "YES" | ||
| 13 | - buildForProfiling = "YES" | ||
| 14 | - buildForArchiving = "YES" | ||
| 15 | - buildForAnalyzing = "YES"> | ||
| 16 | - <BuildableReference | ||
| 17 | - BuildableIdentifier = "primary" | ||
| 18 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 19 | - BuildableName = "Runner Watch App.app" | ||
| 20 | - BlueprintName = "Runner Watch App" | ||
| 21 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 22 | - </BuildableReference> | ||
| 23 | - </BuildActionEntry> | ||
| 24 | - <BuildActionEntry | ||
| 25 | - buildForTesting = "YES" | ||
| 26 | - buildForRunning = "YES" | ||
| 27 | - buildForProfiling = "YES" | ||
| 28 | - buildForArchiving = "YES" | ||
| 29 | - buildForAnalyzing = "YES"> | ||
| 30 | - <BuildableReference | ||
| 31 | - BuildableIdentifier = "primary" | ||
| 32 | - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4" | ||
| 33 | - BuildableName = "Runner.app" | ||
| 34 | - BlueprintName = "Runner" | ||
| 35 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 36 | - </BuildableReference> | ||
| 37 | - </BuildActionEntry> | ||
| 38 | - </BuildActionEntries> | ||
| 39 | - </BuildAction> | ||
| 40 | - <TestAction | ||
| 41 | - buildConfiguration = "Debug" | ||
| 42 | - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
| 43 | - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
| 44 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 45 | - shouldAutocreateTestPlan = "YES"> | ||
| 46 | - </TestAction> | ||
| 47 | - <LaunchAction | ||
| 48 | - buildConfiguration = "Debug" | ||
| 49 | - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
| 50 | - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
| 51 | - launchStyle = "0" | ||
| 52 | - useCustomWorkingDirectory = "NO" | ||
| 53 | - ignoresPersistentStateOnLaunch = "NO" | ||
| 54 | - debugDocumentVersioning = "YES" | ||
| 55 | - debugServiceExtension = "internal" | ||
| 56 | - allowLocationSimulation = "YES"> | ||
| 57 | - <BuildableProductRunnable | ||
| 58 | - runnableDebuggingMode = "0"> | ||
| 59 | - <BuildableReference | ||
| 60 | - BuildableIdentifier = "primary" | ||
| 61 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 62 | - BuildableName = "Runner Watch App.app" | ||
| 63 | - BlueprintName = "Runner Watch App" | ||
| 64 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 65 | - </BuildableReference> | ||
| 66 | - </BuildableProductRunnable> | ||
| 67 | - </LaunchAction> | ||
| 68 | - <ProfileAction | ||
| 69 | - buildConfiguration = "Release" | ||
| 70 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 71 | - savedToolIdentifier = "" | ||
| 72 | - useCustomWorkingDirectory = "NO" | ||
| 73 | - debugDocumentVersioning = "YES"> | ||
| 74 | - <BuildableProductRunnable | ||
| 75 | - runnableDebuggingMode = "0"> | ||
| 76 | - <BuildableReference | ||
| 77 | - BuildableIdentifier = "primary" | ||
| 78 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 79 | - BuildableName = "Runner Watch App.app" | ||
| 80 | - BlueprintName = "Runner Watch App" | ||
| 81 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 82 | - </BuildableReference> | ||
| 83 | - </BuildableProductRunnable> | ||
| 84 | - </ProfileAction> | ||
| 85 | - <AnalyzeAction | ||
| 86 | - buildConfiguration = "Debug"> | ||
| 87 | - </AnalyzeAction> | ||
| 88 | - <ArchiveAction | ||
| 89 | - buildConfiguration = "Release" | ||
| 90 | - revealArchiveInOrganizer = "YES"> | ||
| 91 | - </ArchiveAction> | ||
| 92 | -</Scheme> |
ios/Runner.xcodeproj/xcshareddata/xcschemes/doublefeel-watch-app-extensionExtension.xcscheme
deleted
100644 → 0
| 1 | -<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | -<Scheme | ||
| 3 | - LastUpgradeVersion = "2600" | ||
| 4 | - version = "2.0"> | ||
| 5 | - <BuildAction | ||
| 6 | - parallelizeBuildables = "YES" | ||
| 7 | - buildImplicitDependencies = "YES" | ||
| 8 | - buildArchitectures = "Automatic"> | ||
| 9 | - <BuildActionEntries> | ||
| 10 | - <BuildActionEntry | ||
| 11 | - buildForTesting = "YES" | ||
| 12 | - buildForRunning = "YES" | ||
| 13 | - buildForProfiling = "YES" | ||
| 14 | - buildForArchiving = "YES" | ||
| 15 | - buildForAnalyzing = "YES"> | ||
| 16 | - <BuildableReference | ||
| 17 | - BuildableIdentifier = "primary" | ||
| 18 | - BlueprintIdentifier = "66FBE5A42FDA518C00F515B4" | ||
| 19 | - BuildableName = "doublefeel-watch-app-extensionExtension.appex" | ||
| 20 | - BlueprintName = "doublefeel-watch-app-extensionExtension" | ||
| 21 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 22 | - </BuildableReference> | ||
| 23 | - </BuildActionEntry> | ||
| 24 | - <BuildActionEntry | ||
| 25 | - buildForTesting = "YES" | ||
| 26 | - buildForRunning = "YES" | ||
| 27 | - buildForProfiling = "YES" | ||
| 28 | - buildForArchiving = "YES" | ||
| 29 | - buildForAnalyzing = "YES"> | ||
| 30 | - <BuildableReference | ||
| 31 | - BuildableIdentifier = "primary" | ||
| 32 | - BlueprintIdentifier = "66FBE58A2FDA4F0F00F515B4" | ||
| 33 | - BuildableName = "Runner Watch App.app" | ||
| 34 | - BlueprintName = "Runner Watch App" | ||
| 35 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 36 | - </BuildableReference> | ||
| 37 | - </BuildActionEntry> | ||
| 38 | - <BuildActionEntry | ||
| 39 | - buildForTesting = "YES" | ||
| 40 | - buildForRunning = "YES" | ||
| 41 | - buildForProfiling = "YES" | ||
| 42 | - buildForArchiving = "YES" | ||
| 43 | - buildForAnalyzing = "YES"> | ||
| 44 | - <BuildableReference | ||
| 45 | - BuildableIdentifier = "primary" | ||
| 46 | - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4" | ||
| 47 | - BuildableName = "Runner.app" | ||
| 48 | - BlueprintName = "Runner" | ||
| 49 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 50 | - </BuildableReference> | ||
| 51 | - </BuildActionEntry> | ||
| 52 | - </BuildActionEntries> | ||
| 53 | - </BuildAction> | ||
| 54 | - <TestAction | ||
| 55 | - buildConfiguration = "Debug" | ||
| 56 | - selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
| 57 | - selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
| 58 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 59 | - shouldAutocreateTestPlan = "YES"> | ||
| 60 | - </TestAction> | ||
| 61 | - <LaunchAction | ||
| 62 | - buildConfiguration = "Debug" | ||
| 63 | - selectedDebuggerIdentifier = "" | ||
| 64 | - selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" | ||
| 65 | - launchStyle = "0" | ||
| 66 | - askForAppToLaunch = "Yes" | ||
| 67 | - useCustomWorkingDirectory = "NO" | ||
| 68 | - ignoresPersistentStateOnLaunch = "NO" | ||
| 69 | - debugDocumentVersioning = "YES" | ||
| 70 | - debugServiceExtension = "internal" | ||
| 71 | - allowLocationSimulation = "YES" | ||
| 72 | - launchAutomaticallySubstyle = "2"> | ||
| 73 | - <BuildableProductRunnable | ||
| 74 | - runnableDebuggingMode = "0"> | ||
| 75 | - <BuildableReference | ||
| 76 | - BuildableIdentifier = "primary" | ||
| 77 | - BlueprintIdentifier = "66FBE57D2FDA4F0E00F515B4" | ||
| 78 | - BuildableName = "Runner.app" | ||
| 79 | - BlueprintName = "Runner" | ||
| 80 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 81 | - </BuildableReference> | ||
| 82 | - </BuildableProductRunnable> | ||
| 83 | - <EnvironmentVariables> | ||
| 84 | - <EnvironmentVariable | ||
| 85 | - key = "_XCWidgetKind" | ||
| 86 | - value = "" | ||
| 87 | - isEnabled = "YES"> | ||
| 88 | - </EnvironmentVariable> | ||
| 89 | - <EnvironmentVariable | ||
| 90 | - key = "_XCWidgetDefaultView" | ||
| 91 | - value = "timeline" | ||
| 92 | - isEnabled = "YES"> | ||
| 93 | - </EnvironmentVariable> | ||
| 94 | - <EnvironmentVariable | ||
| 95 | - key = "_XCWidgetFamily" | ||
| 96 | - value = "systemMedium" | ||
| 97 | - isEnabled = "YES"> | ||
| 98 | - </EnvironmentVariable> | ||
| 99 | - </EnvironmentVariables> | ||
| 100 | - </LaunchAction> | ||
| 101 | - <ProfileAction | ||
| 102 | - buildConfiguration = "Release" | ||
| 103 | - shouldUseLaunchSchemeArgsEnv = "YES" | ||
| 104 | - savedToolIdentifier = "" | ||
| 105 | - useCustomWorkingDirectory = "NO" | ||
| 106 | - debugDocumentVersioning = "YES"> | ||
| 107 | - <BuildableProductRunnable | ||
| 108 | - runnableDebuggingMode = "0"> | ||
| 109 | - <BuildableReference | ||
| 110 | - BuildableIdentifier = "primary" | ||
| 111 | - BlueprintIdentifier = "66FBE5A42FDA518C00F515B4" | ||
| 112 | - BuildableName = "doublefeel-watch-app-extensionExtension.appex" | ||
| 113 | - BlueprintName = "doublefeel-watch-app-extensionExtension" | ||
| 114 | - ReferencedContainer = "container:Runner.xcodeproj"> | ||
| 115 | - </BuildableReference> | ||
| 116 | - </BuildableProductRunnable> | ||
| 117 | - </ProfileAction> | ||
| 118 | - <AnalyzeAction | ||
| 119 | - buildConfiguration = "Debug"> | ||
| 120 | - </AnalyzeAction> | ||
| 121 | - <ArchiveAction | ||
| 122 | - buildConfiguration = "Release" | ||
| 123 | - revealArchiveInOrganizer = "YES"> | ||
| 124 | - </ArchiveAction> | ||
| 125 | -</Scheme> |
| 1 | import 'package:doublefeel_flutter/app/widget/dash_divider.dart'; | 1 | import 'package:doublefeel_flutter/app/widget/dash_divider.dart'; |
| 2 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 4 | 5 | ||
| @@ -95,7 +96,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -95,7 +96,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 95 | style: TextStyle( | 96 | style: TextStyle( |
| 96 | fontSize: 20, | 97 | fontSize: 20, |
| 97 | fontWeight: FontWeight.w600, | 98 | fontWeight: FontWeight.w600, |
| 98 | - color: const Color(0xFF0F0F11), | 99 | + color: context.colors.textPrimary, |
| 99 | height: 1.4, | 100 | height: 1.4, |
| 100 | ), | 101 | ), |
| 101 | ), | 102 | ), |
| @@ -110,7 +111,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -110,7 +111,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 110 | borderRadius: BorderRadius.circular(20), | 111 | borderRadius: BorderRadius.circular(20), |
| 111 | boxShadow: [ | 112 | boxShadow: [ |
| 112 | BoxShadow( | 113 | BoxShadow( |
| 113 | - color: const Color(0xFF845EEE).withValues(alpha: 0.08), | 114 | + color: context.colors.primary.withValues(alpha: 0.08), |
| 114 | blurRadius: 24, | 115 | blurRadius: 24, |
| 115 | offset: const Offset(0, 8), | 116 | offset: const Offset(0, 8), |
| 116 | ), | 117 | ), |
| @@ -134,7 +135,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -134,7 +135,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 134 | onTap: controller.onSubmitPartnerId, | 135 | onTap: controller.onSubmitPartnerId, |
| 135 | enabled: controller.canSubmit && !controller.isSubmitting.value, | 136 | enabled: controller.canSubmit && !controller.isSubmitting.value, |
| 136 | isLoading: controller.isSubmitting.value, | 137 | isLoading: controller.isSubmitting.value, |
| 137 | - primaryColor: const Color(0xFFFF9A6E), | 138 | + primaryColor: context.colors.chartOrange, |
| 138 | ); | 139 | ); |
| 139 | }), | 140 | }), |
| 140 | ), | 141 | ), |
| @@ -156,7 +157,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -156,7 +157,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 156 | style: TextStyle( | 157 | style: TextStyle( |
| 157 | fontSize: 14, | 158 | fontSize: 14, |
| 158 | fontWeight: FontWeight.w400, | 159 | fontWeight: FontWeight.w400, |
| 159 | - color: const Color(0xFF0F0F11), | 160 | + color: context.colors.textPrimary, |
| 160 | ), | 161 | ), |
| 161 | ), | 162 | ), |
| 162 | SizedBox(height: 13), | 163 | SizedBox(height: 13), |
| @@ -170,7 +171,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -170,7 +171,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 170 | style: TextStyle( | 171 | style: TextStyle( |
| 171 | fontSize: 32, | 172 | fontSize: 32, |
| 172 | fontWeight: FontWeight.w600, | 173 | fontWeight: FontWeight.w600, |
| 173 | - color: const Color(0xFF845EEE), | 174 | + color: context.colors.primary, |
| 174 | height: 1.4, | 175 | height: 1.4, |
| 175 | ), | 176 | ), |
| 176 | ), | 177 | ), |
| @@ -189,10 +190,9 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -189,10 +190,9 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 189 | color: const Color(0xFFF0F0F6), | 190 | color: const Color(0xFFF0F0F6), |
| 190 | borderRadius: BorderRadius.circular(6), | 191 | borderRadius: BorderRadius.circular(6), |
| 191 | ), | 192 | ), |
| 192 | - child: Icon( | ||
| 193 | - Icons.copy_rounded, | ||
| 194 | - size: 12, | ||
| 195 | - color: const Color(0xFF845EEE), | 193 | + padding: EdgeInsets.all(3), |
| 194 | + child: Image.asset( | ||
| 195 | + 'assets/images/common/ic_copy_purple.webp', | ||
| 196 | ), | 196 | ), |
| 197 | ), | 197 | ), |
| 198 | ), | 198 | ), |
| @@ -205,6 +205,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -205,6 +205,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 205 | label: context.l10n.bindPartnerShareMyCode, | 205 | label: context.l10n.bindPartnerShareMyCode, |
| 206 | onTap: controller.onShareMyCode, | 206 | onTap: controller.onShareMyCode, |
| 207 | enabled: true, | 207 | enabled: true, |
| 208 | + primaryColor: context.colors.primary, | ||
| 208 | ), | 209 | ), |
| 209 | ], | 210 | ], |
| 210 | ), | 211 | ), |
| @@ -233,7 +234,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -233,7 +234,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 233 | style: TextStyle( | 234 | style: TextStyle( |
| 234 | fontSize: 14, | 235 | fontSize: 14, |
| 235 | fontWeight: FontWeight.w400, | 236 | fontWeight: FontWeight.w400, |
| 236 | - color: const Color(0xFF845EEE), | 237 | + color: context.colors.primary, |
| 237 | ), | 238 | ), |
| 238 | ), | 239 | ), |
| 239 | ), | 240 | ), |
| @@ -260,7 +261,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -260,7 +261,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 260 | style: TextStyle( | 261 | style: TextStyle( |
| 261 | fontSize: 14, | 262 | fontSize: 14, |
| 262 | fontWeight: FontWeight.w400, | 263 | fontWeight: FontWeight.w400, |
| 263 | - color: const Color(0xFF0F0F11), | 264 | + color: context.colors.textPrimary, |
| 264 | ), | 265 | ), |
| 265 | ), | 266 | ), |
| 266 | SizedBox(height: 16), | 267 | SizedBox(height: 16), |
| @@ -278,7 +279,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -278,7 +279,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 278 | style: TextStyle( | 279 | style: TextStyle( |
| 279 | fontSize: 32, | 280 | fontSize: 32, |
| 280 | fontWeight: FontWeight.w400, | 281 | fontWeight: FontWeight.w400, |
| 281 | - color: const Color(0xFF0F0F11), | 282 | + color: context.colors.textPrimary, |
| 282 | ), | 283 | ), |
| 283 | decoration: InputDecoration( | 284 | decoration: InputDecoration( |
| 284 | hintText: context.l10n.bindPartnerInputHint, | 285 | hintText: context.l10n.bindPartnerInputHint, |
| @@ -303,7 +304,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -303,7 +304,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 303 | required VoidCallback onTap, | 304 | required VoidCallback onTap, |
| 304 | bool enabled = true, | 305 | bool enabled = true, |
| 305 | bool isLoading = false, | 306 | bool isLoading = false, |
| 306 | - Color primaryColor = const Color(0xFF845EEE), | 307 | + required Color primaryColor, |
| 307 | }) { | 308 | }) { |
| 308 | return GestureDetector( | 309 | return GestureDetector( |
| 309 | onTap: enabled ? onTap : null, | 310 | onTap: enabled ? onTap : null, |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; |
| 4 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 4 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 5 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 5 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 6 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 7 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| @@ -11,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | @@ -11,7 +12,6 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | ||
| 11 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 12 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 12 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; | 13 | import 'package:doublefeel_flutter/data/models/health/health_models.dart'; |
| 13 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; | 14 | import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart'; |
| 14 | -import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | ||
| 15 | import 'package:flutter/material.dart'; | 15 | import 'package:flutter/material.dart'; |
| 16 | import 'package:get/get.dart'; | 16 | import 'package:get/get.dart'; |
| 17 | 17 | ||
| @@ -66,6 +66,9 @@ class TodayController extends GetxController { | @@ -66,6 +66,9 @@ class TodayController extends GetxController { | ||
| 66 | // ── 压力状态 ───────────────────────────── | 66 | // ── 压力状态 ───────────────────────────── |
| 67 | final stressLabel = '状态正常'.obs; | 67 | final stressLabel = '状态正常'.obs; |
| 68 | 68 | ||
| 69 | + /// 进度条压力分(null=加载中, 0~100=压力分: 0优秀, 100过载) | ||
| 70 | + final stressScore = Rxn<int>(); | ||
| 71 | + | ||
| 69 | // ── HRV 表盘引导 Banner ─────────────────── | 72 | // ── HRV 表盘引导 Banner ─────────────────── |
| 70 | final showHrvAdBanner = true.obs; | 73 | final showHrvAdBanner = true.obs; |
| 71 | final showPartnerAdBanner = true.obs; | 74 | final showPartnerAdBanner = true.obs; |
| @@ -77,8 +80,6 @@ class TodayController extends GetxController { | @@ -77,8 +80,6 @@ class TodayController extends GetxController { | ||
| 77 | // ── HRV 数字 + 心率 ─────────────────────── | 80 | // ── HRV 数字 + 心率 ─────────────────────── |
| 78 | final avgHrv = '--'.obs; | 81 | final avgHrv = '--'.obs; |
| 79 | final restingHeartRate = '--'.obs; | 82 | final restingHeartRate = '--'.obs; |
| 80 | - final latestHrvValue = '--'.obs; | ||
| 81 | - final latestHrvTimeLabel = ''.obs; | ||
| 82 | 83 | ||
| 83 | // ── 睡眠卡片 ────────────────────────────── | 84 | // ── 睡眠卡片 ────────────────────────────── |
| 84 | final sleepHours = '--'.obs; | 85 | final sleepHours = '--'.obs; |
| @@ -110,7 +111,12 @@ class TodayController extends GetxController { | @@ -110,7 +111,12 @@ class TodayController extends GetxController { | ||
| 110 | // } catch (error) { | 111 | // } catch (error) { |
| 111 | // debugPrint('Health authorization skipped: $error'); | 112 | // debugPrint('Health authorization skipped: $error'); |
| 112 | // } | 113 | // } |
| 113 | - Get.to(NoHealthDataPage()); | 114 | + Get.to(NoHealthDataPage( |
| 115 | + onRefresh: () {}, | ||
| 116 | + onHelp: () { | ||
| 117 | + Get.toNamed(Routes.HELP); | ||
| 118 | + }, | ||
| 119 | + )); | ||
| 114 | } | 120 | } |
| 115 | 121 | ||
| 116 | @override | 122 | @override |
| @@ -129,8 +135,6 @@ class TodayController extends GetxController { | @@ -129,8 +135,6 @@ class TodayController extends GetxController { | ||
| 129 | unawaited(loadDataForDate(today)); | 135 | unawaited(loadDataForDate(today)); |
| 130 | } | 136 | } |
| 131 | 137 | ||
| 132 | - Future<void> loadTodayData() => loadDataForDate(lastSelectableDay); | ||
| 133 | - | ||
| 134 | void changeDate(DateTime date, {DateTime? focused}) { | 138 | void changeDate(DateTime date, {DateTime? focused}) { |
| 135 | final normalizedDate = _clampDate(date); | 139 | final normalizedDate = _clampDate(date); |
| 136 | selectedDate.value = normalizedDate; | 140 | selectedDate.value = normalizedDate; |
| @@ -148,7 +152,6 @@ class TodayController extends GetxController { | @@ -148,7 +152,6 @@ class TodayController extends GetxController { | ||
| 148 | isLoadingToday.value = true; | 152 | isLoadingToday.value = true; |
| 149 | try { | 153 | try { |
| 150 | await Future.wait([ | 154 | await Future.wait([ |
| 151 | - _refreshUserGreeting(), | ||
| 152 | // _refreshHealthAuthorizationState(), | 155 | // _refreshHealthAuthorizationState(), |
| 153 | _refreshHealthDataForDate(date), | 156 | _refreshHealthDataForDate(date), |
| 154 | ]); | 157 | ]); |
| @@ -161,16 +164,6 @@ class TodayController extends GetxController { | @@ -161,16 +164,6 @@ class TodayController extends GetxController { | ||
| 161 | } | 164 | } |
| 162 | } | 165 | } |
| 163 | 166 | ||
| 164 | - Future<void> _refreshUserGreeting() async { | ||
| 165 | - final result = await _userApi.getUserInfo(errorHandlingPolicy: null); | ||
| 166 | - if (result case AppSuccess<UserInfoResponse>(data: final user)) { | ||
| 167 | - final name = user.nickname?.trim(); | ||
| 168 | - stressSubtitle.value = name == null || name.isEmpty | ||
| 169 | - ? 'Hi, 你今日的综合压力状态' | ||
| 170 | - : 'Hi, $name 今日的综合压力状态'; | ||
| 171 | - } | ||
| 172 | - } | ||
| 173 | - | ||
| 174 | Future<void> _refreshHealthAuthorizationState() async { | 167 | Future<void> _refreshHealthAuthorizationState() async { |
| 175 | final serverAuth = | 168 | final serverAuth = |
| 176 | await _healthApi.checkServerHealthAuth(errorHandlingPolicy: null); | 169 | await _healthApi.checkServerHealthAuth(errorHandlingPolicy: null); |
| @@ -197,6 +190,7 @@ class TodayController extends GetxController { | @@ -197,6 +190,7 @@ class TodayController extends GetxController { | ||
| 197 | const dateRangeType = 1; | 190 | const dateRangeType = 1; |
| 198 | 191 | ||
| 199 | final isToday = DateUtils.isSameDay(date, lastSelectableDay); | 192 | final isToday = DateUtils.isSameDay(date, lastSelectableDay); |
| 193 | + stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态'; | ||
| 200 | 194 | ||
| 201 | final todayResultFuture = isToday | 195 | final todayResultFuture = isToday |
| 202 | ? _healthApi.getTodayData( | 196 | ? _healthApi.getTodayData( |
| @@ -290,9 +284,8 @@ class TodayController extends GetxController { | @@ -290,9 +284,8 @@ class TodayController extends GetxController { | ||
| 290 | stressChartData.clear(); | 284 | stressChartData.clear(); |
| 291 | hrvAnnotations.clear(); | 285 | hrvAnnotations.clear(); |
| 292 | avgHrv.value = '--'; | 286 | avgHrv.value = '--'; |
| 293 | - latestHrvValue.value = '--'; | ||
| 294 | - latestHrvTimeLabel.value = ''; | ||
| 295 | stressLabel.value = '状态正常'; | 287 | stressLabel.value = '状态正常'; |
| 288 | + stressScore.value = null; | ||
| 296 | } | 289 | } |
| 297 | 290 | ||
| 298 | void _applyTodayData(TodayStatusData today) { | 291 | void _applyTodayData(TodayStatusData today) { |
| @@ -331,6 +324,11 @@ class TodayController extends GetxController { | @@ -331,6 +324,11 @@ class TodayController extends GetxController { | ||
| 331 | if (latestStatus != null) { | 324 | if (latestStatus != null) { |
| 332 | stressLabel.value = latestStatus.title; | 325 | stressLabel.value = latestStatus.title; |
| 333 | } | 326 | } |
| 327 | + | ||
| 328 | + // 更新进度条压力分 | ||
| 329 | + final latestHrvVal = latest?.value ?? average; | ||
| 330 | + stressScore.value = null; // _computeStressScore(latestHrvVal); | ||
| 331 | + | ||
| 334 | _updateLatestHrvAnnotation(latest); | 332 | _updateLatestHrvAnnotation(latest); |
| 335 | } | 333 | } |
| 336 | } | 334 | } |
| @@ -340,15 +338,14 @@ class TodayController extends GetxController { | @@ -340,15 +338,14 @@ class TodayController extends GetxController { | ||
| 340 | if (avgHrv.value == '--' && userHrv != null) { | 338 | if (avgHrv.value == '--' && userHrv != null) { |
| 341 | avgHrv.value = _formatMetric(userHrv); | 339 | avgHrv.value = _formatMetric(userHrv); |
| 342 | } | 340 | } |
| 343 | - if (latestHrvValue.value == '--' && userHrv != null) { | ||
| 344 | - latestHrvValue.value = _formatMetric(userHrv); | ||
| 345 | - latestHrvTimeLabel.value = ''; | ||
| 346 | - } | ||
| 347 | if (userHrv != null) { | 341 | if (userHrv != null) { |
| 348 | stressLabel.value = _hrvStatusTitle( | 342 | stressLabel.value = _hrvStatusTitle( |
| 349 | value: userHrv, | 343 | value: userHrv, |
| 350 | baseline: latestHrv.userHrvBaseline, | 344 | baseline: latestHrv.userHrvBaseline, |
| 351 | ); | 345 | ); |
| 346 | + // 更新进度条压力分 | ||
| 347 | + stressScore.value = null; | ||
| 348 | + //_computeStressScore(userHrv,baseline: latestHrv.userHrvBaseline,); | ||
| 352 | } | 349 | } |
| 353 | } | 350 | } |
| 354 | 351 | ||
| @@ -484,11 +481,6 @@ class TodayController extends GetxController { | @@ -484,11 +481,6 @@ class TodayController extends GetxController { | ||
| 484 | void _applyLatestTodayHrv(TodayHrvData? latest) { | 481 | void _applyLatestTodayHrv(TodayHrvData? latest) { |
| 485 | final value = latest?.value; | 482 | final value = latest?.value; |
| 486 | if (value == null) return; | 483 | if (value == null) return; |
| 487 | - | ||
| 488 | - latestHrvValue.value = _formatMetric(value); | ||
| 489 | - final time = latest?.time; | ||
| 490 | - latestHrvTimeLabel.value = | ||
| 491 | - time == null ? '' : _formatHour(_hourFromApiTime(time)); | ||
| 492 | } | 484 | } |
| 493 | 485 | ||
| 494 | void _applySleepDuration(int? rawDuration) { | 486 | void _applySleepDuration(int? rawDuration) { |
| @@ -568,6 +560,28 @@ class TodayController extends GetxController { | @@ -568,6 +560,28 @@ class TodayController extends GetxController { | ||
| 568 | ]); | 560 | ]); |
| 569 | } | 561 | } |
| 570 | 562 | ||
| 563 | + /// 将 HRV 值映射为压力分 0~100(足基线时按相对偏差映射,无基线时按 100-HRV)。 | ||
| 564 | + int _computeStressScore(double hrv, {double? baseline}) { | ||
| 565 | + if ((baseline ?? 0) > 0) { | ||
| 566 | + final ratio = hrv / baseline!; | ||
| 567 | + // ratio≥1.2 → 0~20(优秀) | ||
| 568 | + if (ratio >= 1.2) | ||
| 569 | + return ((1.2 - ratio.clamp(1.2, 1.5)) / 0.3 * 20).round().clamp(0, 20); | ||
| 570 | + // ratio ∈ [0.9,1.2) → 21~50(正常) | ||
| 571 | + if (ratio >= 0.9) | ||
| 572 | + return (((1.2 - ratio) / 0.3) * 29 + 21).round().clamp(21, 50); | ||
| 573 | + // ratio ∈ [0.8,0.9) → 51~80(注意) | ||
| 574 | + if (ratio >= 0.8) | ||
| 575 | + return (((0.9 - ratio) / 0.1) * 29 + 51).round().clamp(51, 80); | ||
| 576 | + // ratio < 0.8 → 81~100(过载) | ||
| 577 | + return ((0.8 - ratio.clamp(0.4, 0.8)) / 0.4 * 19 + 81) | ||
| 578 | + .round() | ||
| 579 | + .clamp(81, 100); | ||
| 580 | + } | ||
| 581 | + // 无基线:100 - HRV,HRV 越高 → 分数越低 | ||
| 582 | + return (100 - hrv).clamp(0, 100).round(); | ||
| 583 | + } | ||
| 584 | + | ||
| 571 | String _hrvStatusTitle({required double value, double? baseline}) { | 585 | String _hrvStatusTitle({required double value, double? baseline}) { |
| 572 | if ((baseline ?? 0) > 0) { | 586 | if ((baseline ?? 0) > 0) { |
| 573 | if (value >= 1.2 * baseline!) return HrvStatus.energetic.title; | 587 | if (value >= 1.2 * baseline!) return HrvStatus.energetic.title; |
| @@ -18,6 +18,7 @@ import '../../widgets/today/today_sleep_activity_cards.dart'; | @@ -18,6 +18,7 @@ import '../../widgets/today/today_sleep_activity_cards.dart'; | ||
| 18 | import '../../widgets/today/stress_status_explanation_bottom_sheet.dart'; | 18 | import '../../widgets/today/stress_status_explanation_bottom_sheet.dart'; |
| 19 | 19 | ||
| 20 | import '../../widgets/today/premium_card.dart'; | 20 | import '../../widgets/today/premium_card.dart'; |
| 21 | +import '../../widgets/today/stress_progress_bar.dart'; | ||
| 21 | 22 | ||
| 22 | class TodayTab extends GetView<TodayController> { | 23 | class TodayTab extends GetView<TodayController> { |
| 23 | const TodayTab({super.key}); | 24 | const TodayTab({super.key}); |
| @@ -99,6 +100,7 @@ class TodayTab extends GetView<TodayController> { | @@ -99,6 +100,7 @@ class TodayTab extends GetView<TodayController> { | ||
| 99 | SliverToBoxAdapter( | 100 | SliverToBoxAdapter( |
| 100 | child: Container( | 101 | child: Container( |
| 101 | height: 323 + pinnedHeaderHeight, | 102 | height: 323 + pinnedHeaderHeight, |
| 103 | + padding: EdgeInsets.only(bottom: 32), | ||
| 102 | decoration: const BoxDecoration( | 104 | decoration: const BoxDecoration( |
| 103 | gradient: LinearGradient( | 105 | gradient: LinearGradient( |
| 104 | begin: Alignment.topCenter, | 106 | begin: Alignment.topCenter, |
| @@ -117,7 +119,9 @@ class TodayTab extends GetView<TodayController> { | @@ -117,7 +119,9 @@ class TodayTab extends GetView<TodayController> { | ||
| 117 | Obx( | 119 | Obx( |
| 118 | () => GestureDetector( | 120 | () => GestureDetector( |
| 119 | behavior: HitTestBehavior.opaque, | 121 | behavior: HitTestBehavior.opaque, |
| 120 | - onTap: showStressStatusExplanationBottomSheet, | 122 | + onTap: () { |
| 123 | + showStressStatusExplanationBottomSheet(context); | ||
| 124 | + }, | ||
| 121 | child: Row( | 125 | child: Row( |
| 122 | mainAxisAlignment: MainAxisAlignment.center, | 126 | mainAxisAlignment: MainAxisAlignment.center, |
| 123 | children: [ | 127 | children: [ |
| @@ -152,6 +156,12 @@ class TodayTab extends GetView<TodayController> { | @@ -152,6 +156,12 @@ class TodayTab extends GetView<TodayController> { | ||
| 152 | ), | 156 | ), |
| 153 | ), | 157 | ), |
| 154 | ), | 158 | ), |
| 159 | + const SizedBox(height: 12), | ||
| 160 | + Obx( | ||
| 161 | + () => StressProgressBar( | ||
| 162 | + score: controller.stressScore.value, | ||
| 163 | + ), | ||
| 164 | + ), | ||
| 155 | ], | 165 | ], |
| 156 | ), | 166 | ), |
| 157 | ), | 167 | ), |
| @@ -456,12 +466,7 @@ class _LatestHrvCard extends GetView<TodayController> { | @@ -456,12 +466,7 @@ class _LatestHrvCard extends GetView<TodayController> { | ||
| 456 | final colors = context.colors; | 466 | final colors = context.colors; |
| 457 | 467 | ||
| 458 | return Obx(() { | 468 | return Obx(() { |
| 459 | - final value = controller.latestHrvValue.value; | ||
| 460 | - final timeLabel = controller.latestHrvTimeLabel.value; | ||
| 461 | - final hrv = _parseMetric(value); | ||
| 462 | - final title = timeLabel.isEmpty ? '最近一次' : '最近一次($timeLabel)'; | ||
| 463 | - final statusLabel = hrv == null ? '暂无状态' : _statusLabel(hrv); | ||
| 464 | - | 469 | + controller.avgHrv.value; |
| 465 | return Container( | 470 | return Container( |
| 466 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), | 471 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), |
| 467 | decoration: BoxDecoration( | 472 | decoration: BoxDecoration( |
| @@ -472,7 +477,7 @@ class _LatestHrvCard extends GetView<TodayController> { | @@ -472,7 +477,7 @@ class _LatestHrvCard extends GetView<TodayController> { | ||
| 472 | crossAxisAlignment: CrossAxisAlignment.start, | 477 | crossAxisAlignment: CrossAxisAlignment.start, |
| 473 | children: [ | 478 | children: [ |
| 474 | Text( | 479 | Text( |
| 475 | - title, | 480 | + '最近一次(17:20)', |
| 476 | style: TextStyle( | 481 | style: TextStyle( |
| 477 | color: colors.textSecondary, | 482 | color: colors.textSecondary, |
| 478 | fontSize: 12, | 483 | fontSize: 12, |
| @@ -486,13 +491,13 @@ class _LatestHrvCard extends GetView<TodayController> { | @@ -486,13 +491,13 @@ class _LatestHrvCard extends GetView<TodayController> { | ||
| 486 | width: 12, | 491 | width: 12, |
| 487 | height: 12, | 492 | height: 12, |
| 488 | decoration: BoxDecoration( | 493 | decoration: BoxDecoration( |
| 489 | - color: _statusColor(context, hrv), | 494 | + color: context.colors.chartGreen, |
| 490 | shape: BoxShape.circle, | 495 | shape: BoxShape.circle, |
| 491 | ), | 496 | ), |
| 492 | ), | 497 | ), |
| 493 | const SizedBox(width: 8), | 498 | const SizedBox(width: 8), |
| 494 | Text( | 499 | Text( |
| 495 | - '${hrv == null ? '--' : value}ms·$statusLabel', | 500 | + '66ms·状态优秀', |
| 496 | style: TextStyle( | 501 | style: TextStyle( |
| 497 | color: colors.textPrimary, | 502 | color: colors.textPrimary, |
| 498 | fontSize: 18, | 503 | fontSize: 18, |
| @@ -503,7 +508,7 @@ class _LatestHrvCard extends GetView<TodayController> { | @@ -503,7 +508,7 @@ class _LatestHrvCard extends GetView<TodayController> { | ||
| 503 | ), | 508 | ), |
| 504 | const SizedBox(height: 8), | 509 | const SizedBox(height: 8), |
| 505 | Text( | 510 | Text( |
| 506 | - '暂无最近一次 HRV 数据。完成一次测量后,这里会展示你的最新状态。', | 511 | + '你的 HRV 高于平时水平。你目前状态放松,压力控制良好——继续保持这样的平衡状态。', |
| 507 | maxLines: 2, | 512 | maxLines: 2, |
| 508 | overflow: TextOverflow.ellipsis, | 513 | overflow: TextOverflow.ellipsis, |
| 509 | style: TextStyle( | 514 | style: TextStyle( |
| @@ -517,29 +522,6 @@ class _LatestHrvCard extends GetView<TodayController> { | @@ -517,29 +522,6 @@ class _LatestHrvCard extends GetView<TodayController> { | ||
| 517 | ); | 522 | ); |
| 518 | }); | 523 | }); |
| 519 | } | 524 | } |
| 520 | - | ||
| 521 | - double? _parseMetric(String value) { | ||
| 522 | - if (value == '--') return null; | ||
| 523 | - return double.tryParse(value.replaceAll(',', '')); | ||
| 524 | - } | ||
| 525 | - | ||
| 526 | - String _statusLabel(double value) { | ||
| 527 | - if (value > 60) return '状态优秀'; | ||
| 528 | - if (value > 50) return '状态良好'; | ||
| 529 | - if (value > 30) return '状态一般'; | ||
| 530 | - if (value > 20) return '状态较差'; | ||
| 531 | - return '状态极差'; | ||
| 532 | - } | ||
| 533 | - | ||
| 534 | - Color _statusColor(BuildContext context, double? value) { | ||
| 535 | - final colors = context.colors; | ||
| 536 | - if (value == null) return colors.textTertiary; | ||
| 537 | - if (value > 60) return colors.chartGreen; | ||
| 538 | - if (value > 50) return colors.chartBlue; | ||
| 539 | - if (value > 30) return colors.chartOrange; | ||
| 540 | - if (value > 20) return colors.chartPink; | ||
| 541 | - return colors.chartBlue; | ||
| 542 | - } | ||
| 543 | } | 525 | } |
| 544 | 526 | ||
| 545 | class _TopDateBar extends StatelessWidget { | 527 | class _TopDateBar extends StatelessWidget { |
| @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | ||
| 5 | 5 | ||
| 6 | import 'today_faq_bottom_sheet.dart'; | 6 | import 'today_faq_bottom_sheet.dart'; |
| 7 | 7 | ||
| 8 | -void showHrvPrincipleExplanationBottomSheet() { | 8 | +void showHrvPrincipleExplanationBottomSheet(BuildContext context) { |
| 9 | + final screenHeight = MediaQuery.of(context).size.height; | ||
| 10 | + final statusBarHeight = MediaQuery.of(context).padding.top; | ||
| 11 | + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight; | ||
| 9 | Get.bottomSheet( | 12 | Get.bottomSheet( |
| 10 | DraggableScrollableSheet( | 13 | DraggableScrollableSheet( |
| 11 | - initialChildSize: 0.96, | ||
| 12 | - minChildSize: 0.2, | ||
| 13 | - maxChildSize: 0.96, | 14 | + maxChildSize: maxChildSize, |
| 15 | + initialChildSize: maxChildSize, | ||
| 14 | expand: false, | 16 | expand: false, |
| 15 | snap: true, | 17 | snap: true, |
| 16 | builder: (context, scrollController) { | 18 | builder: (context, scrollController) { |
| @@ -277,7 +279,7 @@ class _FaqLink extends StatelessWidget { | @@ -277,7 +279,7 @@ class _FaqLink extends StatelessWidget { | ||
| 277 | Widget build(BuildContext context) { | 279 | Widget build(BuildContext context) { |
| 278 | return GestureDetector( | 280 | return GestureDetector( |
| 279 | behavior: HitTestBehavior.opaque, | 281 | behavior: HitTestBehavior.opaque, |
| 280 | - onTap: showTodayFaqBottomSheet, | 282 | + onTap: () => showTodayFaqBottomSheet(), |
| 281 | child: Padding( | 283 | child: Padding( |
| 282 | padding: const EdgeInsets.only(bottom: 14), | 284 | padding: const EdgeInsets.only(bottom: 14), |
| 283 | child: Text( | 285 | child: Text( |
| @@ -229,52 +229,59 @@ class _AuthorizationCard extends StatelessWidget { | @@ -229,52 +229,59 @@ class _AuthorizationCard extends StatelessWidget { | ||
| 229 | Widget build(BuildContext context) { | 229 | Widget build(BuildContext context) { |
| 230 | return _CardShell( | 230 | return _CardShell( |
| 231 | child: Column( | 231 | child: Column( |
| 232 | - crossAxisAlignment: CrossAxisAlignment.start, | ||
| 233 | children: [ | 232 | children: [ |
| 234 | - Center(child: _IconPlaceholder()), | ||
| 235 | - SizedBox(height: 10), | ||
| 236 | - Text( | ||
| 237 | - '错误 2:健康数据访问未授权', | ||
| 238 | - style: TextStyle( | ||
| 239 | - color: Colors.black, | ||
| 240 | - fontSize: 16, | ||
| 241 | - fontWeight: FontWeight.w600, | ||
| 242 | - height: 22 / 16, | ||
| 243 | - ), | ||
| 244 | - ), | ||
| 245 | - SizedBox(height: 6), | ||
| 246 | - Text( | ||
| 247 | - 'DoubleFeel 需要访问 Apple Health 数据的权限,以提供压力统计、提醒和建议。如果未授权,部分功能可能无法正常使用。\n\n请放心,所有健康数据仅存储在本地,不会上传。\n\n要启用权限,请按提示操作,并在 iOS 设置中选择 允许所有 → 健康 → DoubleFeel。', | ||
| 248 | - style: TextStyle( | ||
| 249 | - color: context.colors.textSecondary, | ||
| 250 | - fontSize: 12, | ||
| 251 | - fontWeight: FontWeight.w400, | ||
| 252 | - height: 17 / 12, | ||
| 253 | - ), | 233 | + Column( |
| 234 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 235 | + mainAxisSize: MainAxisSize.min, | ||
| 236 | + children: [ | ||
| 237 | + Center(child: _IconPlaceholder()), | ||
| 238 | + SizedBox(height: 10), | ||
| 239 | + Text( | ||
| 240 | + '错误 2:健康数据访问未授权', | ||
| 241 | + style: TextStyle( | ||
| 242 | + color: Colors.black, | ||
| 243 | + fontSize: 16, | ||
| 244 | + fontWeight: FontWeight.w600, | ||
| 245 | + height: 22 / 16, | ||
| 246 | + ), | ||
| 247 | + ), | ||
| 248 | + SizedBox(height: 6), | ||
| 249 | + Text( | ||
| 250 | + 'DoubleFeel 需要访问 Apple Health 数据的权限,以提供压力统计、提醒和建议。如果未授权,部分功能可能无法正常使用。\n\n请放心,所有健康数据仅存储在本地,不会上传。\n\n要启用权限,请按提示操作,并在 iOS 设置中选择 允许所有 → 健康 → DoubleFeel。', | ||
| 251 | + style: TextStyle( | ||
| 252 | + color: context.colors.textSecondary, | ||
| 253 | + fontSize: 12, | ||
| 254 | + fontWeight: FontWeight.w400, | ||
| 255 | + height: 17 / 12, | ||
| 256 | + ), | ||
| 257 | + ), | ||
| 258 | + ], | ||
| 254 | ), | 259 | ), |
| 255 | SizedBox(height: 37), | 260 | SizedBox(height: 37), |
| 256 | Image.asset( | 261 | Image.asset( |
| 257 | - 'assets/images/today/health_data_permission_step_1.png', | ||
| 258 | - height: 140, | ||
| 259 | - alignment: Alignment(0, 0.12), | 262 | + 'assets/images/permission/apple_privacy_cn.png', |
| 263 | + height: 154, | ||
| 264 | + width: 303, | ||
| 265 | + ), | ||
| 266 | + SizedBox( | ||
| 267 | + height: 10, | ||
| 260 | ), | 268 | ), |
| 261 | - SizedBox(height: 20), | ||
| 262 | Image.asset( | 269 | Image.asset( |
| 263 | - 'assets/images/today/health_data_permission_step_2.png', | 270 | + 'assets/images/permission/apple_health_item_cn.png', |
| 264 | height: 172, | 271 | height: 172, |
| 265 | - alignment: Alignment(0, 0.24), | 272 | + width: 303, |
| 266 | ), | 273 | ), |
| 267 | SizedBox(height: 20), | 274 | SizedBox(height: 20), |
| 268 | Image.asset( | 275 | Image.asset( |
| 269 | - 'assets/images/today/health_data_permission_step_3.png', | 276 | + 'assets/images/permission/apple_double_feel_cn.png', |
| 270 | height: 210, | 277 | height: 210, |
| 271 | - alignment: Alignment(0, -0.13), | 278 | + width: 303, |
| 272 | ), | 279 | ), |
| 273 | SizedBox(height: 20), | 280 | SizedBox(height: 20), |
| 274 | Image.asset( | 281 | Image.asset( |
| 275 | - 'assets/images/today/health_data_permission_step_4.png', | 282 | + 'assets/images/permission/apple_health_step_cn.png', |
| 276 | height: 167, | 283 | height: 167, |
| 277 | - alignment: Alignment(0, -0.54), | 284 | + width: 303, |
| 278 | ), | 285 | ), |
| 279 | ], | 286 | ], |
| 280 | ), | 287 | ), |
| @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | ||
| 5 | 5 | ||
| 6 | import 'today_faq_bottom_sheet.dart'; | 6 | import 'today_faq_bottom_sheet.dart'; |
| 7 | 7 | ||
| 8 | -void showRealtimeStressExplanationBottomSheet() { | 8 | +void showRealtimeStressExplanationBottomSheet(BuildContext context) { |
| 9 | + final screenHeight = MediaQuery.of(context).size.height; | ||
| 10 | + final statusBarHeight = MediaQuery.of(context).padding.top; | ||
| 11 | + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight; | ||
| 9 | Get.bottomSheet( | 12 | Get.bottomSheet( |
| 10 | DraggableScrollableSheet( | 13 | DraggableScrollableSheet( |
| 11 | - initialChildSize: 0.96, | ||
| 12 | - minChildSize: 0.2, | ||
| 13 | - maxChildSize: 0.96, | 14 | + maxChildSize: maxChildSize, |
| 15 | + initialChildSize: maxChildSize, | ||
| 14 | expand: false, | 16 | expand: false, |
| 15 | snap: true, | 17 | snap: true, |
| 16 | builder: (context, scrollController) { | 18 | builder: (context, scrollController) { |
| @@ -309,7 +311,7 @@ class _FaqLink extends StatelessWidget { | @@ -309,7 +311,7 @@ class _FaqLink extends StatelessWidget { | ||
| 309 | Widget build(BuildContext context) { | 311 | Widget build(BuildContext context) { |
| 310 | return GestureDetector( | 312 | return GestureDetector( |
| 311 | behavior: HitTestBehavior.opaque, | 313 | behavior: HitTestBehavior.opaque, |
| 312 | - onTap: showTodayFaqBottomSheet, | 314 | + onTap: () => showTodayFaqBottomSheet(), |
| 313 | child: Padding( | 315 | child: Padding( |
| 314 | padding: const EdgeInsets.only(bottom: 14), | 316 | padding: const EdgeInsets.only(bottom: 14), |
| 315 | child: Text( | 317 | child: Text( |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | + | ||
| 3 | +/// 压力状态进度条(四段式) | ||
| 4 | +/// | ||
| 5 | +/// 对应 Figma node 1361-44236(轨道)和 1361-44489(indicator)。 | ||
| 6 | +/// | ||
| 7 | +/// 传入 [score](0~100 压力分): | ||
| 8 | +/// - null → 加载态:四段等宽 20px,无 indicator | ||
| 9 | +/// - 0~20 → 优秀·绿段(status 3) | ||
| 10 | +/// - 21~50 → 正常·蓝段(status 2) | ||
| 11 | +/// - 51~80 → 注意压力·橙段(status 1) | ||
| 12 | +/// - 81~100 → 压力过载·红段(status 0) | ||
| 13 | +/// | ||
| 14 | +/// indicator 在命中段内的横向位置由分数在该区间的位置决定: | ||
| 15 | +/// 分数越低(越优秀)→ 指针越靠右;分数越高(越差)→ 越靠左。 | ||
| 16 | +/// | ||
| 17 | +/// 段间距 4px,轨道高 14px,indicator 24×24px(6px stroke)。 | ||
| 18 | +class StressProgressBar extends StatelessWidget { | ||
| 19 | + const StressProgressBar({super.key, required this.score}) | ||
| 20 | + : assert(score == null || (score >= 0 && score <= 100)); | ||
| 21 | + | ||
| 22 | + /// null = 加载中;0~100 = 压力分 | ||
| 23 | + final int? score; | ||
| 24 | + | ||
| 25 | + // ── Figma 图表色/1-4(index 0~3 对应 过载→活力)──────── | ||
| 26 | + static const _colors = [ | ||
| 27 | + Color(0xFFFF5279), // 0 压力过载·红 | ||
| 28 | + Color(0xFFFF9A6E), // 1 注意压力·橙 | ||
| 29 | + Color(0xFF7B9BFB), // 2 状态正常·蓝 | ||
| 30 | + Color(0xFF3BD49D), // 3 活力满满·绿 | ||
| 31 | + ]; | ||
| 32 | + | ||
| 33 | + // ── 尺寸常量(来自 Figma)──────────────────────────── | ||
| 34 | + static const _defaultW = 20.0; | ||
| 35 | + static const _activeW = 148.0; | ||
| 36 | + static const _trackH = 14.0; | ||
| 37 | + static const _gap = 4.0; | ||
| 38 | + static const _dotSize = 24.0; | ||
| 39 | + static const _dotBorder = 6.0; | ||
| 40 | + | ||
| 41 | + // ── 动画参数 ────────────────────────────────────────── | ||
| 42 | + static const _dur = Duration(milliseconds: 400); | ||
| 43 | + static const _curve = Curves.easeInOut; | ||
| 44 | + | ||
| 45 | + // ── 分数区间边界(左闭右闭)─────────────────────────── | ||
| 46 | + // status 3 (绿): 0~20 status 2 (蓝): 21~50 | ||
| 47 | + // status 1 (橙): 51~80 status 0 (红): 81~100 | ||
| 48 | + static const _ranges = [ | ||
| 49 | + (lo: 81, hi: 100), // status 0 | ||
| 50 | + (lo: 51, hi: 80), // status 1 | ||
| 51 | + (lo: 21, hi: 50), // status 2 | ||
| 52 | + (lo: 0, hi: 20), // status 3 | ||
| 53 | + ]; | ||
| 54 | + | ||
| 55 | + /// 由压力分计算命中段(0=过载, 1=注意, 2=正常, 3=活力) | ||
| 56 | + static int _statusFromScore(int s) { | ||
| 57 | + if (s <= 20) return 3; | ||
| 58 | + if (s <= 50) return 2; | ||
| 59 | + if (s <= 80) return 1; | ||
| 60 | + return 0; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /// 在命中段内的归一化位置(0.0=最左/最差, 1.0=最右/最优) | ||
| 64 | + /// | ||
| 65 | + /// 分数越低越优秀 → 越靠右,所以 progress = (hi - score) / (hi - lo) | ||
| 66 | + static double _segProgress(int s) { | ||
| 67 | + final st = _statusFromScore(s); | ||
| 68 | + final r = _ranges[st]; | ||
| 69 | + if (r.hi == r.lo) return 0.5; | ||
| 70 | + return ((r.hi - s) / (r.hi - r.lo)).clamp(0.0, 1.0); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + // ── 布局计算 ────────────────────────────────────────── | ||
| 74 | + | ||
| 75 | + int? get _status => score != null ? _statusFromScore(score!) : null; | ||
| 76 | + | ||
| 77 | + /// 第 [i] 段的当前宽度 | ||
| 78 | + double _w(int i) => (_status == i) ? _activeW : _defaultW; | ||
| 79 | + | ||
| 80 | + /// 总宽度(含三个间距) | ||
| 81 | + double get _totalW => _w(0) + _w(1) + _w(2) + _w(3) + 3 * _gap; | ||
| 82 | + | ||
| 83 | + /// indicator 左边缘 X(在命中段内按 segProgress 定位) | ||
| 84 | + double _dotLeft(int s) { | ||
| 85 | + final prog = score != null ? _segProgress(score!) : 0.5; | ||
| 86 | + double x = 0; | ||
| 87 | + for (int i = 0; i < s; i++) { | ||
| 88 | + x += _w(i) + _gap; | ||
| 89 | + } | ||
| 90 | + // 在命中段宽内按 progress 定位,并保证圆点不超出段的边缘 | ||
| 91 | + x += (_w(s) * prog).clamp(_dotSize / 2, _w(s) - _dotSize / 2); | ||
| 92 | + return x - _dotSize / 2; | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + @override | ||
| 96 | + Widget build(BuildContext context) { | ||
| 97 | + final s = _status; | ||
| 98 | + | ||
| 99 | + return AnimatedContainer( | ||
| 100 | + duration: _dur, | ||
| 101 | + curve: _curve, | ||
| 102 | + width: _totalW, | ||
| 103 | + height: _dotSize, | ||
| 104 | + child: Stack( | ||
| 105 | + clipBehavior: Clip.none, | ||
| 106 | + children: [ | ||
| 107 | + // ── 轨道:四段色块 ───────────────────────────── | ||
| 108 | + Positioned( | ||
| 109 | + top: (_dotSize - _trackH) / 2, | ||
| 110 | + left: 0, | ||
| 111 | + child: Row( | ||
| 112 | + mainAxisSize: MainAxisSize.min, | ||
| 113 | + children: [ | ||
| 114 | + for (int i = 0; i < 4; i++) ...[ | ||
| 115 | + AnimatedContainer( | ||
| 116 | + duration: _dur, | ||
| 117 | + curve: _curve, | ||
| 118 | + width: _w(i), | ||
| 119 | + height: _trackH, | ||
| 120 | + decoration: BoxDecoration( | ||
| 121 | + color: _colors[i], | ||
| 122 | + borderRadius: BorderRadius.circular(_trackH / 2), | ||
| 123 | + ), | ||
| 124 | + ), | ||
| 125 | + if (i < 3) const SizedBox(width: _gap), | ||
| 126 | + ], | ||
| 127 | + ], | ||
| 128 | + ), | ||
| 129 | + ), | ||
| 130 | + | ||
| 131 | + // ── Indicator:白底 + 状态色描边 ──────────────── | ||
| 132 | + AnimatedPositioned( | ||
| 133 | + duration: _dur, | ||
| 134 | + curve: _curve, | ||
| 135 | + top: 0, | ||
| 136 | + left: s != null ? _dotLeft(s) : 0, | ||
| 137 | + child: AnimatedOpacity( | ||
| 138 | + opacity: s != null ? 1.0 : 0.0, | ||
| 139 | + duration: _dur, | ||
| 140 | + curve: _curve, | ||
| 141 | + child: Container( | ||
| 142 | + width: _dotSize, | ||
| 143 | + height: _dotSize, | ||
| 144 | + decoration: BoxDecoration( | ||
| 145 | + shape: BoxShape.circle, | ||
| 146 | + color: Colors.white, | ||
| 147 | + border: Border.all( | ||
| 148 | + color: _colors[(s ?? 0).clamp(0, 3)], | ||
| 149 | + width: _dotBorder, | ||
| 150 | + ), | ||
| 151 | + ), | ||
| 152 | + ), | ||
| 153 | + ), | ||
| 154 | + ), | ||
| 155 | + ], | ||
| 156 | + ), | ||
| 157 | + ); | ||
| 158 | + } | ||
| 159 | +} |
| @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | @@ -5,12 +5,14 @@ import 'package:get/get.dart'; | ||
| 5 | 5 | ||
| 6 | import 'today_faq_bottom_sheet.dart'; | 6 | import 'today_faq_bottom_sheet.dart'; |
| 7 | 7 | ||
| 8 | -void showStressStatusExplanationBottomSheet() { | 8 | +void showStressStatusExplanationBottomSheet(BuildContext context) { |
| 9 | + final screenHeight = MediaQuery.of(context).size.height; | ||
| 10 | + final statusBarHeight = MediaQuery.of(context).padding.top; | ||
| 11 | + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight; | ||
| 9 | Get.bottomSheet( | 12 | Get.bottomSheet( |
| 10 | DraggableScrollableSheet( | 13 | DraggableScrollableSheet( |
| 11 | - initialChildSize: 0.96, | ||
| 12 | - minChildSize: 0.2, | ||
| 13 | - maxChildSize: 0.96, | 14 | + maxChildSize: maxChildSize, |
| 15 | + initialChildSize: maxChildSize, | ||
| 14 | expand: false, | 16 | expand: false, |
| 15 | snap: true, | 17 | snap: true, |
| 16 | builder: (context, scrollController) { | 18 | builder: (context, scrollController) { |
| @@ -43,6 +45,9 @@ class StressStatusExplanationBottomSheet extends StatelessWidget { | @@ -43,6 +45,9 @@ class StressStatusExplanationBottomSheet extends StatelessWidget { | ||
| 43 | color: Color(0xFFF5F2FF), | 45 | color: Color(0xFFF5F2FF), |
| 44 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | 46 | borderRadius: BorderRadius.vertical(top: Radius.circular(16)), |
| 45 | ), | 47 | ), |
| 48 | + padding: EdgeInsets.only( | ||
| 49 | + top: MediaQuery.of(context).padding.top, | ||
| 50 | + ), | ||
| 46 | child: SafeArea( | 51 | child: SafeArea( |
| 47 | top: false, | 52 | top: false, |
| 48 | child: Column( | 53 | child: Column( |
| @@ -307,7 +312,7 @@ class _FaqLink extends StatelessWidget { | @@ -307,7 +312,7 @@ class _FaqLink extends StatelessWidget { | ||
| 307 | Widget build(BuildContext context) { | 312 | Widget build(BuildContext context) { |
| 308 | return GestureDetector( | 313 | return GestureDetector( |
| 309 | behavior: HitTestBehavior.opaque, | 314 | behavior: HitTestBehavior.opaque, |
| 310 | - onTap: showTodayFaqBottomSheet, | 315 | + onTap: () => showTodayFaqBottomSheet(), |
| 311 | child: Padding( | 316 | child: Padding( |
| 312 | padding: const EdgeInsets.only(bottom: 14), | 317 | padding: const EdgeInsets.only(bottom: 14), |
| 313 | child: Text( | 318 | child: Text( |
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | -import 'package:get/get.dart'; | ||
| 3 | -import '../../controllers/today_controller.dart'; | ||
| 4 | - | ||
| 5 | -/// Figma: 横向 7 日日期条,当前选中日高亮品牌色圆角背景 | ||
| 6 | -/// 支持左右滑动切换周次(PageView) | ||
| 7 | -class TodayDateStrip extends GetView<TodayController> { | ||
| 8 | - const TodayDateStrip({super.key}); | ||
| 9 | - | ||
| 10 | - static const _brandColor = Color(0xFF845EEE); | ||
| 11 | - static const _h1Color = Color(0xFF0F0F11); | ||
| 12 | - static const _h5Color = Color(0xFFCCCCCC); | ||
| 13 | - | ||
| 14 | - /// 获取以本周一为起点的 7 天列表 | ||
| 15 | - List<DateTime> _getWeekDays(DateTime base) { | ||
| 16 | - final monday = base.subtract(Duration(days: base.weekday - 1)); | ||
| 17 | - return List.generate(7, (i) => monday.add(Duration(days: i))); | ||
| 18 | - } | ||
| 19 | - | ||
| 20 | - String _weekdayLabel(int weekday) { | ||
| 21 | - const labels = ['一', '二', '三', '四', '五', '六', '日']; | ||
| 22 | - return labels[weekday - 1]; | ||
| 23 | - } | ||
| 24 | - | ||
| 25 | - @override | ||
| 26 | - Widget build(BuildContext context) { | ||
| 27 | - return Obx(() { | ||
| 28 | - final selected = controller.selectedDate.value; | ||
| 29 | - final today = DateTime.now(); | ||
| 30 | - final days = _getWeekDays(selected); | ||
| 31 | - | ||
| 32 | - return Container( | ||
| 33 | - height: 72, | ||
| 34 | - decoration: BoxDecoration( | ||
| 35 | - color: _brandColor, | ||
| 36 | - borderRadius: BorderRadius.circular(22), | ||
| 37 | - ), | ||
| 38 | - child: Row( | ||
| 39 | - children: days.map((day) { | ||
| 40 | - final isSelected = _isSameDay(day, selected); | ||
| 41 | - final isToday = _isSameDay(day, today); | ||
| 42 | - final isFuture = day.isAfter(today); | ||
| 43 | - | ||
| 44 | - return Expanded( | ||
| 45 | - child: GestureDetector( | ||
| 46 | - onTap: isFuture ? null : () => controller.changeDate(day), | ||
| 47 | - child: Column( | ||
| 48 | - mainAxisAlignment: MainAxisAlignment.center, | ||
| 49 | - children: [ | ||
| 50 | - // 周几 | ||
| 51 | - Text( | ||
| 52 | - _weekdayLabel(day.weekday), | ||
| 53 | - style: TextStyle( | ||
| 54 | - fontSize: 11, | ||
| 55 | - fontWeight: FontWeight.w400, | ||
| 56 | - color: isFuture | ||
| 57 | - ? _h5Color | ||
| 58 | - : (isSelected ? _h1Color : Colors.white70), | ||
| 59 | - ), | ||
| 60 | - ), | ||
| 61 | - const SizedBox(height: 4), | ||
| 62 | - // 日期数字 | ||
| 63 | - AnimatedContainer( | ||
| 64 | - duration: const Duration(milliseconds: 180), | ||
| 65 | - curve: Curves.easeOut, | ||
| 66 | - width: 28, | ||
| 67 | - height: 28, | ||
| 68 | - decoration: BoxDecoration( | ||
| 69 | - color: isSelected ? Colors.white : Colors.transparent, | ||
| 70 | - borderRadius: BorderRadius.circular(14), | ||
| 71 | - ), | ||
| 72 | - alignment: Alignment.center, | ||
| 73 | - child: Text( | ||
| 74 | - '${day.day}', | ||
| 75 | - style: TextStyle( | ||
| 76 | - fontSize: 15, | ||
| 77 | - fontWeight: isSelected || isToday | ||
| 78 | - ? FontWeight.w600 | ||
| 79 | - : FontWeight.w400, | ||
| 80 | - color: isSelected | ||
| 81 | - ? _brandColor | ||
| 82 | - : isFuture | ||
| 83 | - ? _h5Color | ||
| 84 | - : Colors.white, | ||
| 85 | - ), | ||
| 86 | - ), | ||
| 87 | - ), | ||
| 88 | - ], | ||
| 89 | - ), | ||
| 90 | - ), | ||
| 91 | - ); | ||
| 92 | - }).toList(), | ||
| 93 | - ), | ||
| 94 | - ); | ||
| 95 | - }); | ||
| 96 | - } | ||
| 97 | - | ||
| 98 | - bool _isSameDay(DateTime a, DateTime b) { | ||
| 99 | - return a.year == b.year && a.month == b.month && a.day == b.day; | ||
| 100 | - } | ||
| 101 | -} |
| @@ -4,11 +4,15 @@ import 'package:flutter/material.dart'; | @@ -4,11 +4,15 @@ import 'package:flutter/material.dart'; | ||
| 4 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 5 | 5 | ||
| 6 | void showTodayFaqBottomSheet() { | 6 | void showTodayFaqBottomSheet() { |
| 7 | + final view = WidgetsBinding.instance.platformDispatcher.views.first; | ||
| 8 | + final devicePixelRatio = view.devicePixelRatio; | ||
| 9 | + final screenHeight = view.physicalSize.height / devicePixelRatio; | ||
| 10 | + final statusBarHeight = view.padding.top / devicePixelRatio; | ||
| 11 | + final maxChildSize = (screenHeight - statusBarHeight) / screenHeight; | ||
| 7 | Get.bottomSheet( | 12 | Get.bottomSheet( |
| 8 | DraggableScrollableSheet( | 13 | DraggableScrollableSheet( |
| 9 | - initialChildSize: 0.96, | ||
| 10 | - minChildSize: 0.2, | ||
| 11 | - maxChildSize: 0.96, | 14 | + maxChildSize: maxChildSize, |
| 15 | + initialChildSize: maxChildSize, | ||
| 12 | expand: false, | 16 | expand: false, |
| 13 | snap: true, | 17 | snap: true, |
| 14 | builder: (context, scrollController) { | 18 | builder: (context, scrollController) { |
| 1 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 2 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 2 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 3 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 4 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -65,7 +66,8 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -65,7 +66,8 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 65 | padding: const EdgeInsets.symmetric(horizontal: 2), | 66 | padding: const EdgeInsets.symmetric(horizontal: 2), |
| 66 | child: GestureDetector( | 67 | child: GestureDetector( |
| 67 | behavior: HitTestBehavior.opaque, | 68 | behavior: HitTestBehavior.opaque, |
| 68 | - onTap: showRealtimeStressExplanationBottomSheet, | 69 | + onTap: () => |
| 70 | + showRealtimeStressExplanationBottomSheet(context), | ||
| 69 | child: Image.asset( | 71 | child: Image.asset( |
| 70 | 'assets/images/common/ic_pro_badge.webp', | 72 | 'assets/images/common/ic_pro_badge.webp', |
| 71 | width: 43, | 73 | width: 43, |
| @@ -77,7 +79,8 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -77,7 +79,8 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 77 | padding: const EdgeInsets.symmetric(horizontal: 2), | 79 | padding: const EdgeInsets.symmetric(horizontal: 2), |
| 78 | child: GestureDetector( | 80 | child: GestureDetector( |
| 79 | behavior: HitTestBehavior.opaque, | 81 | behavior: HitTestBehavior.opaque, |
| 80 | - onTap: showRealtimeStressExplanationBottomSheet, | 82 | + onTap: () => |
| 83 | + showRealtimeStressExplanationBottomSheet(context), | ||
| 81 | child: Image.asset( | 84 | child: Image.asset( |
| 82 | 'assets/images/common/ic_info.png', | 85 | 'assets/images/common/ic_info.png', |
| 83 | width: 14, | 86 | width: 14, |
| @@ -91,7 +94,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -91,7 +94,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 91 | ), | 94 | ), |
| 92 | ), | 95 | ), |
| 93 | SizedBox( | 96 | SizedBox( |
| 94 | - height: 174, | 97 | + height: 190, |
| 95 | child: stressPoints.isEmpty | 98 | child: stressPoints.isEmpty |
| 96 | ? const _EmptyChart(text: '暂无压力数据') | 99 | ? const _EmptyChart(text: '暂无压力数据') |
| 97 | : _buildStressChart( | 100 | : _buildStressChart( |
| @@ -117,7 +120,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -117,7 +120,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 117 | const SizedBox(width: 4), | 120 | const SizedBox(width: 4), |
| 118 | GestureDetector( | 121 | GestureDetector( |
| 119 | behavior: HitTestBehavior.opaque, | 122 | behavior: HitTestBehavior.opaque, |
| 120 | - onTap: showHrvPrincipleExplanationBottomSheet, | 123 | + onTap: () => showHrvPrincipleExplanationBottomSheet(context), |
| 121 | child: Image.asset( | 124 | child: Image.asset( |
| 122 | 'assets/images/common/ic_info.png', | 125 | 'assets/images/common/ic_info.png', |
| 123 | width: 14, | 126 | width: 14, |
| @@ -253,130 +256,218 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -253,130 +256,218 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 253 | 256 | ||
| 254 | Widget _buildStressChart( | 257 | Widget _buildStressChart( |
| 255 | BuildContext context, List<HrvDataPoint> stressPoints, bool isVip) { | 258 | BuildContext context, List<HrvDataPoint> stressPoints, bool isVip) { |
| 259 | + void onTapPurchase() => Get.toNamed(Routes.PURCHASE); | ||
| 260 | + final spanWidth = 74.0; | ||
| 256 | return Stack( | 261 | return Stack( |
| 257 | children: [ | 262 | children: [ |
| 258 | - BarChart( | ||
| 259 | - BarChartData( | ||
| 260 | - minY: 0, | ||
| 261 | - maxY: 100, | ||
| 262 | - groupsSpace: 1, | ||
| 263 | - alignment: BarChartAlignment.start, | ||
| 264 | - barGroups: List.generate( | ||
| 265 | - stressPoints.length, | ||
| 266 | - (i) { | ||
| 267 | - final point = stressPoints[i]; | ||
| 268 | - return BarChartGroupData( | ||
| 269 | - x: (point.hour * 10).round(), | ||
| 270 | - barRods: [ | ||
| 271 | - BarChartRodData( | ||
| 272 | - toY: point.hrv, | ||
| 273 | - width: 2, | ||
| 274 | - color: _getColor(point.hrv), | ||
| 275 | - borderRadius: const BorderRadius.only( | ||
| 276 | - topLeft: Radius.circular(2), | ||
| 277 | - topRight: Radius.circular(2), | 263 | + Padding( |
| 264 | + padding: const EdgeInsets.only(top: 6), | ||
| 265 | + child: BarChart( | ||
| 266 | + BarChartData( | ||
| 267 | + minY: 0, | ||
| 268 | + maxY: 100, | ||
| 269 | + groupsSpace: 1, | ||
| 270 | + alignment: BarChartAlignment.start, | ||
| 271 | + barGroups: List.generate( | ||
| 272 | + stressPoints.length, | ||
| 273 | + (i) { | ||
| 274 | + final point = stressPoints[i]; | ||
| 275 | + return BarChartGroupData( | ||
| 276 | + x: (point.hour * 10).round(), | ||
| 277 | + barRods: [ | ||
| 278 | + BarChartRodData( | ||
| 279 | + toY: point.hrv, | ||
| 280 | + width: 2, | ||
| 281 | + color: _getColor(point.hrv), | ||
| 282 | + borderRadius: const BorderRadius.only( | ||
| 283 | + topLeft: Radius.circular(2), | ||
| 284 | + topRight: Radius.circular(2), | ||
| 285 | + ), | ||
| 278 | ), | 286 | ), |
| 279 | - ), | ||
| 280 | - ], | ||
| 281 | - ); | ||
| 282 | - }, | ||
| 283 | - ), | ||
| 284 | - gridData: FlGridData( | ||
| 285 | - show: true, | ||
| 286 | - drawVerticalLine: false, | ||
| 287 | - drawHorizontalLine: true, | ||
| 288 | - horizontalInterval: 25, | ||
| 289 | - getDrawingHorizontalLine: (_) => const FlLine( | ||
| 290 | - color: _h5, | ||
| 291 | - strokeWidth: 1, | ||
| 292 | - dashArray: [2, 2], | 287 | + ], |
| 288 | + ); | ||
| 289 | + }, | ||
| 293 | ), | 290 | ), |
| 294 | - ), | ||
| 295 | - borderData: FlBorderData(show: false), | ||
| 296 | - titlesData: FlTitlesData( | ||
| 297 | - topTitles: | ||
| 298 | - const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 299 | - rightTitles: AxisTitles( | ||
| 300 | - sideTitles: SideTitles( | ||
| 301 | - showTitles: true, | ||
| 302 | - getTitlesWidget: (val, _) { | ||
| 303 | - if (val < 0 || val > 100) { | ||
| 304 | - return const SizedBox.shrink(); | ||
| 305 | - } | ||
| 306 | - return Text( | ||
| 307 | - '${val.toInt()}', | ||
| 308 | - style: TextStyle( | ||
| 309 | - fontSize: 10, color: context.colors.textTertiary), | ||
| 310 | - ); | ||
| 311 | - }, | 291 | + gridData: FlGridData( |
| 292 | + show: true, | ||
| 293 | + drawVerticalLine: false, | ||
| 294 | + drawHorizontalLine: true, | ||
| 295 | + horizontalInterval: 25, | ||
| 296 | + getDrawingHorizontalLine: (_) => const FlLine( | ||
| 297 | + color: _h5, | ||
| 298 | + strokeWidth: 1, | ||
| 299 | + dashArray: [2, 2], | ||
| 312 | ), | 300 | ), |
| 313 | ), | 301 | ), |
| 314 | - leftTitles: | ||
| 315 | - const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 316 | - bottomTitles: AxisTitles( | ||
| 317 | - sideTitles: SideTitles( | ||
| 318 | - showTitles: true, | ||
| 319 | - reservedSize: 20, | ||
| 320 | - interval: 60, | ||
| 321 | - getTitlesWidget: (val, meta) { | ||
| 322 | - final label = _timeLabel(val / 10); | ||
| 323 | - if (label == null) return const SizedBox.shrink(); | ||
| 324 | - return SideTitleWidget( | ||
| 325 | - meta: meta, | ||
| 326 | - child: Text( | ||
| 327 | - label, | 302 | + borderData: FlBorderData(show: false), |
| 303 | + titlesData: FlTitlesData( | ||
| 304 | + topTitles: | ||
| 305 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 306 | + leftTitles: | ||
| 307 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 308 | + rightTitles: AxisTitles( | ||
| 309 | + sideTitles: SideTitles( | ||
| 310 | + showTitles: true, | ||
| 311 | + interval: 50, | ||
| 312 | + reservedSize: 22, | ||
| 313 | + getTitlesWidget: (val, _) { | ||
| 314 | + if (val < 0 || val > 100) { | ||
| 315 | + return const SizedBox.shrink(); | ||
| 316 | + } | ||
| 317 | + return Text( | ||
| 318 | + '${val.toInt()}', | ||
| 328 | style: TextStyle( | 319 | style: TextStyle( |
| 329 | fontSize: 10, color: context.colors.textTertiary), | 320 | fontSize: 10, color: context.colors.textTertiary), |
| 321 | + ); | ||
| 322 | + }, | ||
| 323 | + ), | ||
| 324 | + ), | ||
| 325 | + bottomTitles: AxisTitles( | ||
| 326 | + sideTitles: SideTitles( | ||
| 327 | + showTitles: true, | ||
| 328 | + reservedSize: 22, | ||
| 329 | + interval: 60, | ||
| 330 | + getTitlesWidget: (val, meta) { | ||
| 331 | + final label = _timeLabel(val / 10); | ||
| 332 | + if (label == null) return const SizedBox.shrink(); | ||
| 333 | + return SideTitleWidget( | ||
| 334 | + meta: meta, | ||
| 335 | + child: Text( | ||
| 336 | + label, | ||
| 337 | + style: TextStyle( | ||
| 338 | + fontSize: 10, color: context.colors.textTertiary), | ||
| 339 | + ), | ||
| 340 | + ); | ||
| 341 | + }, | ||
| 342 | + ), | ||
| 343 | + ), | ||
| 344 | + ), | ||
| 345 | + barTouchData: BarTouchData( | ||
| 346 | + touchTooltipData: BarTouchTooltipData( | ||
| 347 | + tooltipRoundedRadius: 8, | ||
| 348 | + tooltipBorder: BorderSide.none, | ||
| 349 | + getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3), | ||
| 350 | + tooltipPadding: const EdgeInsets.only( | ||
| 351 | + left: 12, | ||
| 352 | + right: 12, | ||
| 353 | + top: 6, | ||
| 354 | + bottom: 5, | ||
| 355 | + ), | ||
| 356 | + getTooltipItem: (group, groupIndex, rod, rodIndex) { | ||
| 357 | + return BarTooltipItem( | ||
| 358 | + '', | ||
| 359 | + const TextStyle( | ||
| 360 | + color: Colors.white, | ||
| 361 | + fontWeight: FontWeight.bold, | ||
| 330 | ), | 362 | ), |
| 363 | + children: _getTooltip(rod), | ||
| 364 | + textAlign: TextAlign.start, | ||
| 331 | ); | 365 | ); |
| 332 | }, | 366 | }, |
| 333 | ), | 367 | ), |
| 334 | ), | 368 | ), |
| 335 | ), | 369 | ), |
| 336 | - barTouchData: BarTouchData( | ||
| 337 | - touchTooltipData: BarTouchTooltipData( | ||
| 338 | - tooltipRoundedRadius: 8, | ||
| 339 | - tooltipBorder: BorderSide.none, | ||
| 340 | - getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3), | ||
| 341 | - tooltipPadding: const EdgeInsets.only( | ||
| 342 | - left: 12, | ||
| 343 | - right: 12, | ||
| 344 | - top: 6, | ||
| 345 | - bottom: 5, | ||
| 346 | - ), | ||
| 347 | - getTooltipItem: (group, groupIndex, rod, rodIndex) { | ||
| 348 | - return BarTooltipItem( | ||
| 349 | - '', | ||
| 350 | - const TextStyle( | ||
| 351 | - color: Colors.white, | ||
| 352 | - fontWeight: FontWeight.bold, | ||
| 353 | - ), | ||
| 354 | - children: _getTooltip(rod), | ||
| 355 | - textAlign: TextAlign.start, | ||
| 356 | - ); | ||
| 357 | - }, | 370 | + ), |
| 371 | + ), | ||
| 372 | + Positioned( | ||
| 373 | + left: 0, | ||
| 374 | + top: 14, | ||
| 375 | + bottom: 22, | ||
| 376 | + child: Container( | ||
| 377 | + width: spanWidth, | ||
| 378 | + height: 174, | ||
| 379 | + decoration: const BoxDecoration( | ||
| 380 | + gradient: LinearGradient( | ||
| 381 | + begin: Alignment(0.50, -0.00), | ||
| 382 | + end: Alignment(0.50, 1.00), | ||
| 383 | + colors: [Color(0x4C835DED), Color(0x00845EEE)], | ||
| 358 | ), | 384 | ), |
| 359 | ), | 385 | ), |
| 360 | ), | 386 | ), |
| 361 | ), | 387 | ), |
| 362 | - Container( | ||
| 363 | - width: 74, | ||
| 364 | - height: 174, | ||
| 365 | - decoration: const BoxDecoration( | ||
| 366 | - gradient: LinearGradient( | ||
| 367 | - begin: Alignment(0.50, -0.00), | ||
| 368 | - end: Alignment(0.50, 1.00), | ||
| 369 | - colors: [Color(0x4C835DED), Color(0x00845EEE)], | ||
| 370 | - ), | 388 | + Positioned( |
| 389 | + left: 0, | ||
| 390 | + top: 14, | ||
| 391 | + width: spanWidth, | ||
| 392 | + height: 2, | ||
| 393 | + child: ColoredBox(color: context.colors.primary), | ||
| 394 | + ), | ||
| 395 | + Positioned( | ||
| 396 | + left: spanWidth / 2 - 6, | ||
| 397 | + top: 0, | ||
| 398 | + child: Image.asset( | ||
| 399 | + 'assets/images/common/ic_health_bed.webp', | ||
| 400 | + width: 12, | ||
| 401 | + height: 12, | ||
| 371 | ), | 402 | ), |
| 372 | ), | 403 | ), |
| 373 | - if (isVip == false) | ||
| 374 | - Image.asset( | ||
| 375 | - 'assets/images/today/bg_unlock_instant_stress_blur.png', | ||
| 376 | - width: double.infinity, | ||
| 377 | - height: 174, | ||
| 378 | - fit: BoxFit.cover, | ||
| 379 | - ) | 404 | + if (isVip == false) ...[ |
| 405 | + GestureDetector( | ||
| 406 | + onTap: onTapPurchase, | ||
| 407 | + child: Image.asset( | ||
| 408 | + 'assets/images/today/bg_unlock_instant_stress_blur.png', | ||
| 409 | + width: double.infinity, | ||
| 410 | + height: 174, | ||
| 411 | + fit: BoxFit.cover, | ||
| 412 | + ), | ||
| 413 | + ), | ||
| 414 | + Positioned( | ||
| 415 | + top: 97, | ||
| 416 | + left: 0, | ||
| 417 | + right: 0, | ||
| 418 | + child: Column( | ||
| 419 | + mainAxisSize: MainAxisSize.min, | ||
| 420 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 421 | + children: [ | ||
| 422 | + Row( | ||
| 423 | + mainAxisSize: MainAxisSize.max, | ||
| 424 | + mainAxisAlignment: MainAxisAlignment.center, | ||
| 425 | + children: [ | ||
| 426 | + Image.asset( | ||
| 427 | + 'assets/images/common/ic_lock.png', | ||
| 428 | + width: 12, | ||
| 429 | + height: 12, | ||
| 430 | + color: context.colors.textTertiary, | ||
| 431 | + ), | ||
| 432 | + Text( | ||
| 433 | + '会员可查看完整数据', | ||
| 434 | + style: TextStyle( | ||
| 435 | + color: context.colors.textTertiary, | ||
| 436 | + fontSize: 14, | ||
| 437 | + fontWeight: FontWeight.w400, | ||
| 438 | + ), | ||
| 439 | + ), | ||
| 440 | + ], | ||
| 441 | + ), | ||
| 442 | + GestureDetector( | ||
| 443 | + onTap: onTapPurchase, | ||
| 444 | + child: Container( | ||
| 445 | + constraints: const BoxConstraints( | ||
| 446 | + minWidth: 120, | ||
| 447 | + ), | ||
| 448 | + margin: const EdgeInsets.only(top: 16), | ||
| 449 | + padding: const EdgeInsets.symmetric( | ||
| 450 | + horizontal: 16, vertical: 8), | ||
| 451 | + decoration: ShapeDecoration( | ||
| 452 | + color: const Color(0xFFFFDF50), | ||
| 453 | + shape: RoundedRectangleBorder( | ||
| 454 | + borderRadius: BorderRadius.circular(24), | ||
| 455 | + ), | ||
| 456 | + ), | ||
| 457 | + child: const Text( | ||
| 458 | + '立即解锁', | ||
| 459 | + textAlign: TextAlign.center, | ||
| 460 | + style: TextStyle( | ||
| 461 | + color: Color(0xFF0F0F11), | ||
| 462 | + fontSize: 14, | ||
| 463 | + fontWeight: FontWeight.w500, | ||
| 464 | + ), | ||
| 465 | + ), | ||
| 466 | + ), | ||
| 467 | + ) | ||
| 468 | + ]), | ||
| 469 | + ), | ||
| 470 | + ] | ||
| 380 | ], | 471 | ], |
| 381 | ); | 472 | ); |
| 382 | } | 473 | } |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 1 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 2 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| @@ -51,9 +52,6 @@ class _NumberItem extends StatelessWidget { | @@ -51,9 +52,6 @@ class _NumberItem extends StatelessWidget { | ||
| 51 | required this.unit, | 52 | required this.unit, |
| 52 | }); | 53 | }); |
| 53 | 54 | ||
| 54 | - static const _h1 = Color(0xFF0F0F11); | ||
| 55 | - static const _h2 = Color(0xFF666666); | ||
| 56 | - | ||
| 57 | @override | 55 | @override |
| 58 | Widget build(BuildContext context) { | 56 | Widget build(BuildContext context) { |
| 59 | return Padding( | 57 | return Padding( |
| @@ -63,9 +61,9 @@ class _NumberItem extends StatelessWidget { | @@ -63,9 +61,9 @@ class _NumberItem extends StatelessWidget { | ||
| 63 | children: [ | 61 | children: [ |
| 64 | Text( | 62 | Text( |
| 65 | label, | 63 | label, |
| 66 | - style: const TextStyle( | 64 | + style: TextStyle( |
| 67 | fontSize: 12, | 65 | fontSize: 12, |
| 68 | - color: _h2, | 66 | + color: context.colors.textTertiary, |
| 69 | fontWeight: FontWeight.w400, | 67 | fontWeight: FontWeight.w400, |
| 70 | ), | 68 | ), |
| 71 | ), | 69 | ), |
| @@ -75,10 +73,10 @@ class _NumberItem extends StatelessWidget { | @@ -75,10 +73,10 @@ class _NumberItem extends StatelessWidget { | ||
| 75 | children: [ | 73 | children: [ |
| 76 | Text( | 74 | Text( |
| 77 | value, | 75 | value, |
| 78 | - style: const TextStyle( | 76 | + style: TextStyle( |
| 79 | fontSize: 28, | 77 | fontSize: 28, |
| 80 | fontWeight: FontWeight.w700, | 78 | fontWeight: FontWeight.w700, |
| 81 | - color: _h1, | 79 | + color: context.colors.textPrimary, |
| 82 | height: 1, | 80 | height: 1, |
| 83 | ), | 81 | ), |
| 84 | ), | 82 | ), |
| @@ -87,9 +85,9 @@ class _NumberItem extends StatelessWidget { | @@ -87,9 +85,9 @@ class _NumberItem extends StatelessWidget { | ||
| 87 | padding: const EdgeInsets.only(bottom: 3), | 85 | padding: const EdgeInsets.only(bottom: 3), |
| 88 | child: Text( | 86 | child: Text( |
| 89 | unit, | 87 | unit, |
| 90 | - style: const TextStyle( | 88 | + style: TextStyle( |
| 91 | fontSize: 13, | 89 | fontSize: 13, |
| 92 | - color: _h1, | 90 | + color: context.colors.textPrimary, |
| 93 | fontWeight: FontWeight.w400, | 91 | fontWeight: FontWeight.w400, |
| 94 | ), | 92 | ), |
| 95 | ), | 93 | ), |
| 1 | +import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/combine.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 2 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 2 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 3 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| @@ -46,6 +47,26 @@ class TodaySleepCard extends GetView<TodayController> { | @@ -46,6 +47,26 @@ class TodaySleepCard extends GetView<TodayController> { | ||
| 46 | ], | 47 | ], |
| 47 | ), | 48 | ), |
| 48 | ], | 49 | ], |
| 50 | + rightWidget: Container( | ||
| 51 | + width: 50, | ||
| 52 | + height: 50, | ||
| 53 | + margin: EdgeInsets.only(right: 6, left: 4), | ||
| 54 | + child: CircularGradientCombineWidget( | ||
| 55 | + size: 50, | ||
| 56 | + duration: const Duration(seconds: 1), | ||
| 57 | + sweepAngles: const [240, 210], | ||
| 58 | + backgroundColors: const [ | ||
| 59 | + Color(0xFFF3F3F3), | ||
| 60 | + Color(0xFFF3F3F3), | ||
| 61 | + ], | ||
| 62 | + gradientColors: [ | ||
| 63 | + [context.colors.primary], | ||
| 64 | + [context.colors.chartBlue], | ||
| 65 | + ], | ||
| 66 | + centerCircleSizeRatio: 0.3, | ||
| 67 | + gapRatio: 0.1, | ||
| 68 | + interpolatedColorRatio: 1), | ||
| 69 | + ), | ||
| 49 | ), | 70 | ), |
| 50 | ); | 71 | ); |
| 51 | } | 72 | } |
| @@ -108,6 +129,48 @@ class TodayActivityCard extends GetView<TodayController> { | @@ -108,6 +129,48 @@ class TodayActivityCard extends GetView<TodayController> { | ||
| 108 | ], | 129 | ], |
| 109 | ), | 130 | ), |
| 110 | ], | 131 | ], |
| 132 | + rightWidget: Container( | ||
| 133 | + width: 50, | ||
| 134 | + height: 50, | ||
| 135 | + margin: EdgeInsets.only(right: 6, left: 4), | ||
| 136 | + child: Stack( | ||
| 137 | + children: [ | ||
| 138 | + Container( | ||
| 139 | + decoration: BoxDecoration( | ||
| 140 | + shape: BoxShape.circle, | ||
| 141 | + color: context.colors.textPrimary, | ||
| 142 | + ), | ||
| 143 | + ), | ||
| 144 | + CircularGradientCombineWidget( | ||
| 145 | + size: 50, | ||
| 146 | + duration: const Duration(seconds: 1), | ||
| 147 | + centerCircleSizeRatio: 0.2, | ||
| 148 | + gapRatio: 0.06, | ||
| 149 | + sweepAngles: const [ | ||
| 150 | + 180, | ||
| 151 | + 230, | ||
| 152 | + 180, | ||
| 153 | + ], | ||
| 154 | + backgroundColors: [ | ||
| 155 | + context.colors.chartPink.withOpacity(0.4), | ||
| 156 | + context.colors.chartGreen.withOpacity(0.3), | ||
| 157 | + context.colors.chartBlue.withOpacity(0.4), | ||
| 158 | + ], | ||
| 159 | + gradientColors: [ | ||
| 160 | + [ | ||
| 161 | + context.colors.chartPink, | ||
| 162 | + ], | ||
| 163 | + [ | ||
| 164 | + context.colors.chartGreen, | ||
| 165 | + ], | ||
| 166 | + [ | ||
| 167 | + context.colors.chartBlue, | ||
| 168 | + ], | ||
| 169 | + ], | ||
| 170 | + ), | ||
| 171 | + ], | ||
| 172 | + ), | ||
| 173 | + ), | ||
| 111 | ), | 174 | ), |
| 112 | ); | 175 | ); |
| 113 | } | 176 | } |
| @@ -122,6 +185,7 @@ class _TodaySummaryCard extends StatelessWidget { | @@ -122,6 +185,7 @@ class _TodaySummaryCard extends StatelessWidget { | ||
| 122 | required this.metrics, | 185 | required this.metrics, |
| 123 | required this.progressValues, | 186 | required this.progressValues, |
| 124 | required this.progressColors, | 187 | required this.progressColors, |
| 188 | + required this.rightWidget, | ||
| 125 | }); | 189 | }); |
| 126 | 190 | ||
| 127 | final String iconAsset; | 191 | final String iconAsset; |
| @@ -131,6 +195,7 @@ class _TodaySummaryCard extends StatelessWidget { | @@ -131,6 +195,7 @@ class _TodaySummaryCard extends StatelessWidget { | ||
| 131 | final List<_MetricData> metrics; | 195 | final List<_MetricData> metrics; |
| 132 | final List<double> progressValues; | 196 | final List<double> progressValues; |
| 133 | final List<Color> progressColors; | 197 | final List<Color> progressColors; |
| 198 | + final Widget rightWidget; | ||
| 134 | 199 | ||
| 135 | @override | 200 | @override |
| 136 | Widget build(BuildContext context) { | 201 | Widget build(BuildContext context) { |
| @@ -194,13 +259,7 @@ class _TodaySummaryCard extends StatelessWidget { | @@ -194,13 +259,7 @@ class _TodaySummaryCard extends StatelessWidget { | ||
| 194 | ], | 259 | ], |
| 195 | ), | 260 | ), |
| 196 | ), | 261 | ), |
| 197 | - const SizedBox(width: 4), | ||
| 198 | - Container( | ||
| 199 | - width: 50, | ||
| 200 | - height: 50, | ||
| 201 | - color: Colors.red, | ||
| 202 | - ), | ||
| 203 | - const SizedBox(width: 6), | 262 | + rightWidget |
| 204 | ], | 263 | ], |
| 205 | ), | 264 | ), |
| 206 | ], | 265 | ], |
| @@ -7,7 +7,6 @@ import 'package:get/get.dart'; | @@ -7,7 +7,6 @@ import 'package:get/get.dart'; | ||
| 7 | 7 | ||
| 8 | import '../controllers/user_onboarding_controller.dart'; | 8 | import '../controllers/user_onboarding_controller.dart'; |
| 9 | import '../models/onboarding_option_data.dart'; | 9 | import '../models/onboarding_option_data.dart'; |
| 10 | -import '../widget/guide_common_scaffold.dart'; | ||
| 11 | import '../widget/onboarding_common_widgets.dart'; | 10 | import '../widget/onboarding_common_widgets.dart'; |
| 12 | import '../widget/onboarding_illustrations.dart'; | 11 | import '../widget/onboarding_illustrations.dart'; |
| 13 | import '../widget/onboarding_em_rich_text.dart'; | 12 | import '../widget/onboarding_em_rich_text.dart'; |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'widget.dart'; | ||
| 3 | + | ||
| 4 | +///CircularGradientCombineWidget multi CircularGradientProgressWidget | ||
| 5 | +class CircularGradientCombineWidget extends StatefulWidget { | ||
| 6 | + ///widget size | ||
| 7 | + final double size; | ||
| 8 | + | ||
| 9 | + ///list of each sweep angle | ||
| 10 | + final List<double> sweepAngles; | ||
| 11 | + | ||
| 12 | + ///List<Color> of each circular | ||
| 13 | + final List<List<Color>> gradientColors; | ||
| 14 | + | ||
| 15 | + ///Background Color of each circular | ||
| 16 | + final List<Color> backgroundColors; | ||
| 17 | + | ||
| 18 | + ///empty center circle size ratio by size | ||
| 19 | + final double centerCircleSizeRatio; | ||
| 20 | + | ||
| 21 | + ///each gap of circular ratio by size | ||
| 22 | + final double gapRatio; | ||
| 23 | + | ||
| 24 | + /// default begin angle | ||
| 25 | + final double initAngle; | ||
| 26 | + | ||
| 27 | + ///open animate | ||
| 28 | + final bool animate; | ||
| 29 | + | ||
| 30 | + /// If true, a smoother intermediate color will be automatically inserted between the two gradient colors. | ||
| 31 | + final bool interpolatedColor; | ||
| 32 | + | ||
| 33 | + /// Set the percentage of the gradient color based on the two colors when inserting the intermediate gradient color | ||
| 34 | + final double interpolatedColorRatio; | ||
| 35 | + | ||
| 36 | + /// When the change reaches a certain percentage, the color will be the same as the last color, | ||
| 37 | + //so it needs to be limited, otherwise a circular_gradient_progress with the same color will appear instead of a gradient color. | ||
| 38 | + final double maxInterpolatedColorRatio; | ||
| 39 | + | ||
| 40 | + /// base duration | ||
| 41 | + final Duration duration; | ||
| 42 | + | ||
| 43 | + /// This value represents how much animation time needs to be increased for each 360 degrees after exceeding 360 degrees. | ||
| 44 | + final Duration plusDuration; | ||
| 45 | + | ||
| 46 | + /// max duration | ||
| 47 | + final Duration maxDuration; | ||
| 48 | + | ||
| 49 | + /// animation behavier | ||
| 50 | + final Curve curve; | ||
| 51 | + | ||
| 52 | + /// if true,be reverse | ||
| 53 | + final bool reverse; | ||
| 54 | + | ||
| 55 | + /// | ||
| 56 | + const CircularGradientCombineWidget({ | ||
| 57 | + super.key, | ||
| 58 | + required this.size, | ||
| 59 | + required this.sweepAngles, | ||
| 60 | + required this.backgroundColors, | ||
| 61 | + required this.gradientColors, | ||
| 62 | + this.animate = true, | ||
| 63 | + this.centerCircleSizeRatio = 0.15, | ||
| 64 | + this.gapRatio = 0.05, | ||
| 65 | + this.interpolatedColor = true, | ||
| 66 | + this.interpolatedColorRatio = 0.1, | ||
| 67 | + this.maxInterpolatedColorRatio = 0.7, | ||
| 68 | + this.duration = const Duration(milliseconds: 1500), | ||
| 69 | + this.plusDuration = const Duration(milliseconds: 500), | ||
| 70 | + this.curve = Curves.easeInOutQuad, | ||
| 71 | + this.initAngle = 0, | ||
| 72 | + this.reverse = false, | ||
| 73 | + this.maxDuration = Duration.zero, | ||
| 74 | + }); | ||
| 75 | + | ||
| 76 | + @override | ||
| 77 | + State<CircularGradientCombineWidget> createState() => _CircularGradientCombineWidgetState(); | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +class _CircularGradientCombineWidgetState extends State<CircularGradientCombineWidget> with SingleTickerProviderStateMixin { | ||
| 81 | + @override | ||
| 82 | + Widget build(BuildContext context) { | ||
| 83 | + final centerCircleSize = widget.size * widget.centerCircleSizeRatio; | ||
| 84 | + final gap = widget.size * widget.gapRatio; | ||
| 85 | + var strokeWidth = (widget.size - (widget.sweepAngles.length - 1) * gap - centerCircleSize) / widget.sweepAngles.length; | ||
| 86 | + if (strokeWidth > widget.size / 3) { | ||
| 87 | + strokeWidth = widget.size / 3; | ||
| 88 | + } | ||
| 89 | + return Stack( | ||
| 90 | + alignment: Alignment.center, | ||
| 91 | + children: List.generate(widget.sweepAngles.length, (index) { | ||
| 92 | + return CircularGradientProgressWidget( | ||
| 93 | + animate: widget.animate, | ||
| 94 | + interpolatedColorRatio: widget.interpolatedColorRatio, | ||
| 95 | + maxInterpolatedColorRatio: widget.maxInterpolatedColorRatio, | ||
| 96 | + duration: widget.duration, | ||
| 97 | + plusDuration: widget.plusDuration, | ||
| 98 | + curve: widget.curve, | ||
| 99 | + initAngle: widget.initAngle, | ||
| 100 | + interpolatedColor: widget.interpolatedColor, | ||
| 101 | + gradientColors: index >= widget.gradientColors.length ? widget.gradientColors.last : widget.gradientColors[index], | ||
| 102 | + sweepAngle: index >= widget.sweepAngles.length ? widget.sweepAngles.last : widget.sweepAngles[index], | ||
| 103 | + size: widget.size - strokeWidth * index - gap * index, | ||
| 104 | + strokeWidth: strokeWidth / 2, | ||
| 105 | + backgroundColor: index > widget.backgroundColors.length - 1 ? widget.backgroundColors.last : widget.backgroundColors[index], | ||
| 106 | + reverse: widget.reverse, | ||
| 107 | + maxDuration: widget.maxDuration, | ||
| 108 | + ); | ||
| 109 | + }), | ||
| 110 | + ); | ||
| 111 | + } | ||
| 112 | +} |
| 1 | +import 'dart:math' show cos, min, pi, sin; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | + | ||
| 5 | +/// convert | ||
| 6 | +extension NumToRadians on num { | ||
| 7 | + /// toRadians | ||
| 8 | + double toRadians() { | ||
| 9 | + return toDouble() * (pi / 180.0); | ||
| 10 | + } | ||
| 11 | + | ||
| 12 | + /// toDegree | ||
| 13 | + double toDegree() { | ||
| 14 | + return toDouble() * (180 / pi); | ||
| 15 | + } | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +/// painter for progress | ||
| 19 | +class ProgressPainter extends CustomPainter { | ||
| 20 | + ///0 to max | ||
| 21 | + final double sweepAngle; | ||
| 22 | + | ||
| 23 | + /// circle stroke width | ||
| 24 | + final double strokeWidth; | ||
| 25 | + | ||
| 26 | + /// multi color | ||
| 27 | + final List<List<Color>> progressColor; | ||
| 28 | + | ||
| 29 | + /// base background color | ||
| 30 | + final Color backgroundColor; | ||
| 31 | + | ||
| 32 | + /// reverse | ||
| 33 | + final bool reverse; | ||
| 34 | + | ||
| 35 | + /// painter for progress | ||
| 36 | + ProgressPainter({ | ||
| 37 | + this.sweepAngle = 360, | ||
| 38 | + this.strokeWidth = 20, | ||
| 39 | + this.reverse = false, | ||
| 40 | + required this.progressColor, | ||
| 41 | + required this.backgroundColor, | ||
| 42 | + }); | ||
| 43 | + | ||
| 44 | + @override | ||
| 45 | + void paint(Canvas canvas, Size size) { | ||
| 46 | + double wh = min(size.width, size.height); | ||
| 47 | + var centerX = wh / 2; | ||
| 48 | + var centerY = wh / 2; | ||
| 49 | + var radius = wh / 2 - strokeWidth / 2; | ||
| 50 | + | ||
| 51 | + double startAngle = 0; | ||
| 52 | + var rect = Rect.fromCenter( | ||
| 53 | + center: Offset(centerX, centerY), | ||
| 54 | + width: wh - strokeWidth, | ||
| 55 | + height: wh - strokeWidth, | ||
| 56 | + ); | ||
| 57 | + int colorRangeIndex = (sweepAngle / 360).ceil() - 1; | ||
| 58 | + if (colorRangeIndex > progressColor.length - 1) { | ||
| 59 | + colorRangeIndex = progressColor.length - 1; | ||
| 60 | + } | ||
| 61 | + if (colorRangeIndex < 0) { | ||
| 62 | + colorRangeIndex = 0; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + var shader = SweepGradient( | ||
| 66 | + startAngle: startAngle.toRadians(), | ||
| 67 | + endAngle: 360.toRadians(), | ||
| 68 | + colors: reverse ? progressColor[colorRangeIndex].reversed.toList() : progressColor[colorRangeIndex], | ||
| 69 | + ).createShader(rect); | ||
| 70 | + | ||
| 71 | + var paint = Paint() | ||
| 72 | + ..strokeWidth = strokeWidth | ||
| 73 | + ..shader = shader | ||
| 74 | + ..strokeCap = StrokeCap.butt | ||
| 75 | + ..style = PaintingStyle.stroke; | ||
| 76 | + | ||
| 77 | + var backgroundPaint = Paint() | ||
| 78 | + ..strokeWidth = strokeWidth | ||
| 79 | + ..color = backgroundColor | ||
| 80 | + ..strokeCap = StrokeCap.butt | ||
| 81 | + ..style = PaintingStyle.stroke; | ||
| 82 | + | ||
| 83 | + var startCapPosition = Offset( | ||
| 84 | + centerX + radius * cos(startAngle.toRadians()), | ||
| 85 | + centerY + radius * sin(startAngle.toRadians()), | ||
| 86 | + ); | ||
| 87 | + | ||
| 88 | + canvas.drawArc( | ||
| 89 | + rect, | ||
| 90 | + 0.toRadians(), | ||
| 91 | + 360.toRadians(), | ||
| 92 | + false, | ||
| 93 | + backgroundPaint, | ||
| 94 | + ); | ||
| 95 | + if (sweepAngle >= 0) { | ||
| 96 | + canvas.save(); | ||
| 97 | + // Move the canvas origin to the center | ||
| 98 | + canvas.translate(centerX, centerY); | ||
| 99 | + double rotationAngle = 0; | ||
| 100 | + | ||
| 101 | + if (sweepAngle > 360) { | ||
| 102 | + rotationAngle = 360 - sweepAngle; | ||
| 103 | + } | ||
| 104 | + double rotationRadians = reverse ? -rotationAngle.toRadians() : rotationAngle.toRadians(); | ||
| 105 | + // Rotate the canvas | ||
| 106 | + canvas.rotate((-90).toRadians() - rotationRadians); | ||
| 107 | + // Move the canvas origin back | ||
| 108 | + canvas.translate(-centerX, -centerY); | ||
| 109 | + | ||
| 110 | + if (sweepAngle < 360) { | ||
| 111 | + canvas.drawArc( | ||
| 112 | + Rect.fromCenter( | ||
| 113 | + center: startCapPosition, | ||
| 114 | + width: strokeWidth, | ||
| 115 | + height: strokeWidth, | ||
| 116 | + ), | ||
| 117 | + 0, | ||
| 118 | + reverse ? 180.toRadians() : -180.toRadians(), | ||
| 119 | + true, | ||
| 120 | + Paint()..color = progressColor[0].first, | ||
| 121 | + ); | ||
| 122 | + } | ||
| 123 | + canvas.drawArc( | ||
| 124 | + rect, | ||
| 125 | + 0.toRadians(), | ||
| 126 | + reverse ? -(startAngle + sweepAngle).toRadians() : (startAngle + sweepAngle).toRadians(), | ||
| 127 | + false, | ||
| 128 | + paint, | ||
| 129 | + ); | ||
| 130 | + if (sweepAngle < 360) { | ||
| 131 | + canvas.drawRect( | ||
| 132 | + Rect.fromCenter( | ||
| 133 | + center: startCapPosition, | ||
| 134 | + width: strokeWidth, | ||
| 135 | + height: 1, | ||
| 136 | + ), | ||
| 137 | + Paint()..color = progressColor[0].first, | ||
| 138 | + ); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + canvas.translate(centerX, centerY); | ||
| 142 | + if (sweepAngle < 360) { | ||
| 143 | + final endCapRotate = 360 - sweepAngle; | ||
| 144 | + canvas.rotate(reverse ? endCapRotate.toRadians() : -endCapRotate.toRadians()); | ||
| 145 | + } | ||
| 146 | + //0.1 is because the angle calculation is not accurate enough, so we need to go back a little bit. | ||
| 147 | + var endCapPosition = Offset( | ||
| 148 | + centerX + radius * cos((startAngle).toRadians()), | ||
| 149 | + centerY + radius * sin((startAngle).toRadians()), | ||
| 150 | + ); | ||
| 151 | + | ||
| 152 | + canvas.translate(-centerX, -centerY); | ||
| 153 | + final endColor = getColorAtAngle(sweepAngle, progressColor[colorRangeIndex]); | ||
| 154 | + if (sweepAngle > 355) { | ||
| 155 | + //Draw arc shadow | ||
| 156 | + var shadowPosition = Offset( | ||
| 157 | + centerX + radius * cos((startAngle).toRadians()), | ||
| 158 | + centerY + (reverse ? -5 : 5) + radius * sin((startAngle).toRadians()), | ||
| 159 | + ); | ||
| 160 | + final Paint shadowPaint = Paint() | ||
| 161 | + ..color = Colors.black.withOpacity(0.2) | ||
| 162 | + ..style = PaintingStyle.stroke | ||
| 163 | + ..strokeWidth = strokeWidth / 2 | ||
| 164 | + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3); | ||
| 165 | + final Path path = Path() | ||
| 166 | + ..addArc( | ||
| 167 | + Rect.fromCenter( | ||
| 168 | + center: shadowPosition, | ||
| 169 | + width: strokeWidth / 2, | ||
| 170 | + height: strokeWidth / 2, | ||
| 171 | + ), | ||
| 172 | + 0, | ||
| 173 | + reverse ? -pi : pi, | ||
| 174 | + ); | ||
| 175 | + canvas.drawPath(path, shadowPaint); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + //Draw the ending arc | ||
| 179 | + canvas.drawArc( | ||
| 180 | + Rect.fromCenter( | ||
| 181 | + center: endCapPosition, | ||
| 182 | + width: strokeWidth, | ||
| 183 | + height: strokeWidth, | ||
| 184 | + ), | ||
| 185 | + 0, | ||
| 186 | + reverse ? -180.toRadians() : 180.toRadians(), | ||
| 187 | + true, | ||
| 188 | + Paint()..color = endColor, | ||
| 189 | + ); | ||
| 190 | + canvas.drawRect( | ||
| 191 | + Rect.fromCenter( | ||
| 192 | + center: endCapPosition, | ||
| 193 | + width: strokeWidth, | ||
| 194 | + height: 1, | ||
| 195 | + ), | ||
| 196 | + Paint()..color = endColor, | ||
| 197 | + ); | ||
| 198 | + | ||
| 199 | + // // Restore the canvas to the saved state | ||
| 200 | + canvas.restore(); | ||
| 201 | + } | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + @override | ||
| 205 | + bool shouldRepaint(covariant ProgressPainter oldDelegate) { | ||
| 206 | + return sweepAngle != oldDelegate.sweepAngle; | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + /// get color by and angle | ||
| 210 | + Color getColorAtAngle(double angle, List<Color> colors) { | ||
| 211 | + double totalAngle = sweepAngle > 360 ? sweepAngle : 360; | ||
| 212 | + double fraction = angle / totalAngle; | ||
| 213 | + int segments = colors.length - 1; | ||
| 214 | + double segmentFraction = fraction * segments; | ||
| 215 | + int segmentIndex = segmentFraction.floor(); | ||
| 216 | + double segmentInnerFraction = segmentFraction - segmentIndex; | ||
| 217 | + Color startColor = colors[segmentIndex]; | ||
| 218 | + Color endColor = colors[(segmentIndex + 1) % colors.length]; | ||
| 219 | + return Color.lerp(startColor, endColor, segmentInnerFraction)!; | ||
| 220 | + } | ||
| 221 | +} |
| 1 | +import 'dart:math'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | + | ||
| 5 | +import 'paint.dart'; | ||
| 6 | + | ||
| 7 | +/// CircularGradientProgressWidget | ||
| 8 | +class CircularGradientProgressWidget extends StatefulWidget { | ||
| 9 | + ///widget size | ||
| 10 | + final double size; | ||
| 11 | + | ||
| 12 | + ///circle size | ||
| 13 | + final double strokeWidth; | ||
| 14 | + | ||
| 15 | + ///need > 0 ,< 0 means no data | ||
| 16 | + final double sweepAngle; | ||
| 17 | + | ||
| 18 | + /// default begin angle | ||
| 19 | + final double initAngle; | ||
| 20 | + | ||
| 21 | + ///The colors need to go from light to dark, otherwise the effect is weird. | ||
| 22 | + ///Receive multiple color arrays so that more colors are needed when reaching a certain angle. | ||
| 23 | + final List<Color> gradientColors; | ||
| 24 | + | ||
| 25 | + /// color of when angle is zero | ||
| 26 | + final Color backgroundColor; | ||
| 27 | + | ||
| 28 | + ///open animate | ||
| 29 | + final bool animate; | ||
| 30 | + | ||
| 31 | + /// If true, a smoother intermediate color will be automatically inserted between the two gradient colors. | ||
| 32 | + final bool interpolatedColor; | ||
| 33 | + | ||
| 34 | + /// Set the percentage of the gradient color based on the two colors when inserting the intermediate gradient color | ||
| 35 | + final double interpolatedColorRatio; | ||
| 36 | + | ||
| 37 | + /// When the change reaches a certain percentage, the color will be the same as the last color, | ||
| 38 | + //so it needs to be limited, otherwise a circular_gradient_progress with the same color will appear instead of a gradient color. | ||
| 39 | + final double maxInterpolatedColorRatio; | ||
| 40 | + | ||
| 41 | + /// base duration | ||
| 42 | + final Duration duration; | ||
| 43 | + | ||
| 44 | + /// This value represents how much animation time needs to be increased for each 360 degrees after exceeding 360 degrees. | ||
| 45 | + final Duration plusDuration; | ||
| 46 | + | ||
| 47 | + /// max duration | ||
| 48 | + final Duration maxDuration; | ||
| 49 | + | ||
| 50 | + /// animation behavier | ||
| 51 | + final Curve curve; | ||
| 52 | + | ||
| 53 | + /// if true,be reverse | ||
| 54 | + final bool reverse; | ||
| 55 | + | ||
| 56 | + /// | ||
| 57 | + const CircularGradientProgressWidget({ | ||
| 58 | + super.key, | ||
| 59 | + required this.gradientColors, | ||
| 60 | + required this.backgroundColor, | ||
| 61 | + required this.size, | ||
| 62 | + this.sweepAngle = 0, | ||
| 63 | + required this.strokeWidth, | ||
| 64 | + this.animate = false, | ||
| 65 | + this.interpolatedColor = true, | ||
| 66 | + this.interpolatedColorRatio = 0.1, | ||
| 67 | + this.maxInterpolatedColorRatio = 0.7, | ||
| 68 | + this.duration = const Duration(milliseconds: 1500), | ||
| 69 | + this.plusDuration = const Duration(milliseconds: 500), | ||
| 70 | + this.curve = Curves.easeInOutQuad, | ||
| 71 | + this.initAngle = 0, | ||
| 72 | + this.reverse = false, | ||
| 73 | + this.maxDuration = Duration.zero, | ||
| 74 | + }); | ||
| 75 | + | ||
| 76 | + @override | ||
| 77 | + State<CircularGradientProgressWidget> createState() => _CircularGradientProgressWidgetState(); | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +class _CircularGradientProgressWidgetState extends State<CircularGradientProgressWidget> with SingleTickerProviderStateMixin { | ||
| 81 | + late AnimationController _controller; | ||
| 82 | + late Animation<double> _animation; | ||
| 83 | + double sweepAngle = 0; | ||
| 84 | + double initAngle = 0; | ||
| 85 | + | ||
| 86 | + @override | ||
| 87 | + void initState() { | ||
| 88 | + super.initState(); | ||
| 89 | + initAngle = widget.initAngle; | ||
| 90 | + if (widget.sweepAngle < 0) { | ||
| 91 | + initAngle = -1; | ||
| 92 | + } | ||
| 93 | + _controller = AnimationController( | ||
| 94 | + vsync: this, | ||
| 95 | + duration: widget.duration, | ||
| 96 | + ); | ||
| 97 | + _updateAnimation(widget.sweepAngle, initAngle); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @override | ||
| 101 | + void didUpdateWidget(CircularGradientProgressWidget oldWidget) { | ||
| 102 | + super.didUpdateWidget(oldWidget); | ||
| 103 | + if (widget.sweepAngle != oldWidget.sweepAngle) { | ||
| 104 | + _updateAnimation(widget.sweepAngle, sweepAngle); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + void _updateAnimation(double newSweepAngle, double oldSweepAngle) { | ||
| 109 | + if (widget.sweepAngle < 0 || newSweepAngle < 0) { | ||
| 110 | + setState(() { | ||
| 111 | + sweepAngle = -1; | ||
| 112 | + }); | ||
| 113 | + return; | ||
| 114 | + } | ||
| 115 | + final double angleDifference = (newSweepAngle - oldSweepAngle).abs(); | ||
| 116 | + int baseDurationMillis = widget.duration.inMilliseconds; | ||
| 117 | + | ||
| 118 | + int totalDuration = 0; | ||
| 119 | + if (angleDifference <= 360 && oldSweepAngle != 0) { | ||
| 120 | + totalDuration = ((angleDifference / 360) * baseDurationMillis).toInt(); | ||
| 121 | + } else { | ||
| 122 | + totalDuration = baseDurationMillis; | ||
| 123 | + int full360Intervals = (angleDifference / 360).floor(); | ||
| 124 | + totalDuration += full360Intervals * widget.plusDuration.inMilliseconds; | ||
| 125 | + } | ||
| 126 | + if (widget.animate) { | ||
| 127 | + if (widget.maxDuration.inMilliseconds != 0 && totalDuration > widget.maxDuration.inMilliseconds) { | ||
| 128 | + _controller.duration = Duration(milliseconds: widget.maxDuration.inMilliseconds); | ||
| 129 | + } else { | ||
| 130 | + _controller.duration = Duration(milliseconds: totalDuration); | ||
| 131 | + } | ||
| 132 | + } else { | ||
| 133 | + _controller.duration = Duration.zero; | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + _animation = Tween<double>(begin: oldSweepAngle, end: newSweepAngle).animate( | ||
| 137 | + CurvedAnimation( | ||
| 138 | + parent: _controller, | ||
| 139 | + curve: widget.curve, | ||
| 140 | + ), | ||
| 141 | + )..addListener(() { | ||
| 142 | + setState(() { | ||
| 143 | + sweepAngle = _animation.value; | ||
| 144 | + }); | ||
| 145 | + }); | ||
| 146 | + _controller.forward(from: 0); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + @override | ||
| 150 | + void dispose() { | ||
| 151 | + _controller.dispose(); | ||
| 152 | + super.dispose(); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + List<List<Color>> generateColorArray(List<Color> colorList, double totalAngle) { | ||
| 156 | + final baseColorStart = colorList[0]; | ||
| 157 | + final baseColorEnd = colorList.length > 1 ? colorList[1] : colorList[0]; | ||
| 158 | + List<List<Color>> colors = []; | ||
| 159 | + int fullRotations = (totalAngle / 360).floor(); | ||
| 160 | + colors.add([baseColorStart, baseColorEnd]); | ||
| 161 | + for (int i = 1; i <= fullRotations; i++) { | ||
| 162 | + double t = min(i * widget.interpolatedColorRatio, widget.maxInterpolatedColorRatio); | ||
| 163 | + colors.add([Color.lerp(baseColorStart, baseColorEnd, t)!, baseColorEnd]); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + return colors; | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + @override | ||
| 170 | + Widget build(BuildContext context) { | ||
| 171 | + final progressColors = generateColorArray(widget.gradientColors, widget.interpolatedColor ? sweepAngle : 0); | ||
| 172 | + return CustomPaint( | ||
| 173 | + painter: ProgressPainter( | ||
| 174 | + sweepAngle: sweepAngle, | ||
| 175 | + progressColor: progressColors, | ||
| 176 | + strokeWidth: widget.strokeWidth, | ||
| 177 | + backgroundColor: widget.backgroundColor, | ||
| 178 | + reverse: widget.reverse, | ||
| 179 | + ), | ||
| 180 | + size: Size( | ||
| 181 | + widget.size, | ||
| 182 | + widget.size, | ||
| 183 | + ), | ||
| 184 | + ); | ||
| 185 | + } | ||
| 186 | +} |
| @@ -61,9 +61,9 @@ | @@ -61,9 +61,9 @@ | ||
| 61 | "onboardingResearchEnergy": "元气满满", | 61 | "onboardingResearchEnergy": "元气满满", |
| 62 | "onboardingResearchHrvDown": "HRV下降", | 62 | "onboardingResearchHrvDown": "HRV下降", |
| 63 | "onboardingResearchHrvUp": "HRV上升", | 63 | "onboardingResearchHrvUp": "HRV上升", |
| 64 | - "onboardingHealthPermissionTitle": "允许访问健康数据", | 64 | + "onboardingHealthPermissionTitle": "开启健康授权提醒、统计你的压力", |
| 65 | "onboardingHealthPermissionBody": "DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。", | 65 | "onboardingHealthPermissionBody": "DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。", |
| 66 | - "onboardingHealthPermissionPrivacy": "请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。", | 66 | + "onboardingHealthPermissionPrivacy": "请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。", |
| 67 | "onboardingNotificationTitle": "开启通知", | 67 | "onboardingNotificationTitle": "开启通知", |
| 68 | "onboardingNotificationSubtitle": "及时了解身体每一次异动", | 68 | "onboardingNotificationSubtitle": "及时了解身体每一次异动", |
| 69 | "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态", | 69 | "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态", |
| @@ -446,7 +446,7 @@ abstract class AppLocalizations { | @@ -446,7 +446,7 @@ abstract class AppLocalizations { | ||
| 446 | /// No description provided for @onboardingHealthPermissionTitle. | 446 | /// No description provided for @onboardingHealthPermissionTitle. |
| 447 | /// | 447 | /// |
| 448 | /// In zh, this message translates to: | 448 | /// In zh, this message translates to: |
| 449 | - /// **'允许访问健康数据'** | 449 | + /// **'开启健康授权提醒、统计你的压力'** |
| 450 | String get onboardingHealthPermissionTitle; | 450 | String get onboardingHealthPermissionTitle; |
| 451 | 451 | ||
| 452 | /// No description provided for @onboardingHealthPermissionBody. | 452 | /// No description provided for @onboardingHealthPermissionBody. |
| @@ -458,7 +458,7 @@ abstract class AppLocalizations { | @@ -458,7 +458,7 @@ abstract class AppLocalizations { | ||
| 458 | /// No description provided for @onboardingHealthPermissionPrivacy. | 458 | /// No description provided for @onboardingHealthPermissionPrivacy. |
| 459 | /// | 459 | /// |
| 460 | /// In zh, this message translates to: | 460 | /// In zh, this message translates to: |
| 461 | - /// **'请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'** | 461 | + /// **'请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。'** |
| 462 | String get onboardingHealthPermissionPrivacy; | 462 | String get onboardingHealthPermissionPrivacy; |
| 463 | 463 | ||
| 464 | /// No description provided for @onboardingNotificationTitle. | 464 | /// No description provided for @onboardingNotificationTitle. |
| 1 | +// ignore: unused_import | ||
| 2 | +import 'package:intl/intl.dart' as intl; | ||
| 1 | import 'app_localizations.dart'; | 3 | import 'app_localizations.dart'; |
| 2 | 4 | ||
| 3 | // ignore_for_file: type=lint | 5 | // ignore_for_file: type=lint |
| @@ -70,7 +72,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -70,7 +72,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 70 | String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app'; | 72 | String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app'; |
| 71 | 73 | ||
| 72 | @override | 74 | @override |
| 73 | - String get onboardingIntroBody => '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>'; | 75 | + String get onboardingIntroBody => |
| 76 | + '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>'; | ||
| 74 | 77 | ||
| 75 | @override | 78 | @override |
| 76 | String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?'; | 79 | String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?'; |
| @@ -160,7 +163,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -160,7 +163,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 160 | String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态'; | 163 | String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态'; |
| 161 | 164 | ||
| 162 | @override | 165 | @override |
| 163 | - String get onboardingHrvDescription => '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力'; | 166 | + String get onboardingHrvDescription => |
| 167 | + '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力'; | ||
| 164 | 168 | ||
| 165 | @override | 169 | @override |
| 166 | String get onboardingTellMeMore => '展开说说'; | 170 | String get onboardingTellMeMore => '展开说说'; |
| @@ -181,13 +185,15 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -181,13 +185,15 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 181 | String get onboardingResearchHrvUp => 'HRV上升'; | 185 | String get onboardingResearchHrvUp => 'HRV上升'; |
| 182 | 186 | ||
| 183 | @override | 187 | @override |
| 184 | - String get onboardingHealthPermissionTitle => '允许访问健康数据'; | 188 | + String get onboardingHealthPermissionTitle => '开启健康授权提醒、统计你的压力'; |
| 185 | 189 | ||
| 186 | @override | 190 | @override |
| 187 | - String get onboardingHealthPermissionBody => 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。'; | 191 | + String get onboardingHealthPermissionBody => |
| 192 | + 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。'; | ||
| 188 | 193 | ||
| 189 | @override | 194 | @override |
| 190 | - String get onboardingHealthPermissionPrivacy => '请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'; | 195 | + String get onboardingHealthPermissionPrivacy => |
| 196 | + '请放心,你的健康数据仅储存在本地,我们不上传任何相关数据。'; | ||
| 191 | 197 | ||
| 192 | @override | 198 | @override |
| 193 | String get onboardingNotificationTitle => '开启通知'; | 199 | String get onboardingNotificationTitle => '开启通知'; |
| @@ -196,7 +202,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -196,7 +202,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 196 | String get onboardingNotificationSubtitle => '及时了解身体每一次异动'; | 202 | String get onboardingNotificationSubtitle => '及时了解身体每一次异动'; |
| 197 | 203 | ||
| 198 | @override | 204 | @override |
| 199 | - String get onboardingNotificationBody => 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态'; | 205 | + String get onboardingNotificationBody => |
| 206 | + 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态'; | ||
| 200 | 207 | ||
| 201 | @override | 208 | @override |
| 202 | String get onboardingMemberTitle => '获得年度会员优惠'; | 209 | String get onboardingMemberTitle => '获得年度会员优惠'; |
| @@ -220,7 +227,9 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -220,7 +227,9 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 220 | String get healthCompanionIsNowAvailable => '健康陪伴已开启'; | 227 | String get healthCompanionIsNowAvailable => '健康陪伴已开启'; |
| 221 | 228 | ||
| 222 | @override | 229 | @override |
| 223 | - String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'; | 230 | + String |
| 231 | + get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => | ||
| 232 | + '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'; | ||
| 224 | 233 | ||
| 225 | @override | 234 | @override |
| 226 | String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康'; | 235 | String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康'; |
| @@ -333,7 +342,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -333,7 +342,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 333 | String get todayHealthDataAuthTitle => '无法获取心率健康数据'; | 342 | String get todayHealthDataAuthTitle => '无法获取心率健康数据'; |
| 334 | 343 | ||
| 335 | @override | 344 | @override |
| 336 | - String get todayHealthDataAuthDescription => 'DoubleFeel 需要授权访问你的健康数据,才能提供压力提醒、实时压力统计和健康建议;否则应用功能可能无法正常使用。请放心,你的健康数据仅存储在本地,不会上传到任何服务器。'; | 345 | + String get todayHealthDataAuthDescription => |
| 346 | + 'DoubleFeel 需要授权访问你的健康数据,才能提供压力提醒、实时压力统计和健康建议;否则应用功能可能无法正常使用。请放心,你的健康数据仅存储在本地,不会上传到任何服务器。'; | ||
| 337 | 347 | ||
| 338 | @override | 348 | @override |
| 339 | String get todayHealthDataAuthAction => '授权访问健康数据'; | 349 | String get todayHealthDataAuthAction => '授权访问健康数据'; |
| @@ -360,7 +370,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -360,7 +370,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 360 | String get todayFaqLinkWatchNoStatusNotification => '手表为什么无法收到状态通知?'; | 370 | String get todayFaqLinkWatchNoStatusNotification => '手表为什么无法收到状态通知?'; |
| 361 | 371 | ||
| 362 | @override | 372 | @override |
| 363 | - String get todayFaqLinkWatchNoStatusAndInteractionNotification => '手表为什么无法收到状态和互动通知?'; | 373 | + String get todayFaqLinkWatchNoStatusAndInteractionNotification => |
| 374 | + '手表为什么无法收到状态和互动通知?'; | ||
| 364 | 375 | ||
| 365 | @override | 376 | @override |
| 366 | String get todayFaqLinkWatchFaceDataDelay => '手表表盘数据不更新或者有延迟?'; | 377 | String get todayFaqLinkWatchFaceDataDelay => '手表表盘数据不更新或者有延迟?'; |
| @@ -393,22 +404,27 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -393,22 +404,27 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 393 | String get todayStressStatusInsufficientData => '数据不足'; | 404 | String get todayStressStatusInsufficientData => '数据不足'; |
| 394 | 405 | ||
| 395 | @override | 406 | @override |
| 396 | - String get todayStressStatusOverloadDescription => '当前 HRV 明显低于你的长期平均水平,可能意味着身体疲劳、压力过高或恢复不足。建议及时休息。'; | 407 | + String get todayStressStatusOverloadDescription => |
| 408 | + '当前 HRV 明显低于你的长期平均水平,可能意味着身体疲劳、压力过高或恢复不足。建议及时休息。'; | ||
| 397 | 409 | ||
| 398 | @override | 410 | @override |
| 399 | - String get todayStressStatusCautionDescription => '当前 HRV 低于正常范围,身体可能正在积累压力,需要注意作息与恢复。'; | 411 | + String get todayStressStatusCautionDescription => |
| 412 | + '当前 HRV 低于正常范围,身体可能正在积累压力,需要注意作息与恢复。'; | ||
| 400 | 413 | ||
| 401 | @override | 414 | @override |
| 402 | String get todayStressStatusNormalDescription => '当前身体状态处于你的正常波动范围内。'; | 415 | String get todayStressStatusNormalDescription => '当前身体状态处于你的正常波动范围内。'; |
| 403 | 416 | ||
| 404 | @override | 417 | @override |
| 405 | - String get todayStressStatusExcellentDescription => '当前 HRV 高于近期平均水平,代表身体恢复与整体状态较好。'; | 418 | + String get todayStressStatusExcellentDescription => |
| 419 | + '当前 HRV 高于近期平均水平,代表身体恢复与整体状态较好。'; | ||
| 406 | 420 | ||
| 407 | @override | 421 | @override |
| 408 | - String get todayStressStatusInsufficientDataDescription => '当前可用数据不足,暂时无法准确判断压力状态。'; | 422 | + String get todayStressStatusInsufficientDataDescription => |
| 423 | + '当前可用数据不足,暂时无法准确判断压力状态。'; | ||
| 409 | 424 | ||
| 410 | @override | 425 | @override |
| 411 | - String get todayHrvMeasurementIntro => 'AppleWatch默认每2-5小时测量一次HRV,如果你希望立即手动进行测量,可以参考以下方法:'; | 426 | + String get todayHrvMeasurementIntro => |
| 427 | + 'AppleWatch默认每2-5小时测量一次HRV,如果你希望立即手动进行测量,可以参考以下方法:'; | ||
| 412 | 428 | ||
| 413 | @override | 429 | @override |
| 414 | String get todayHrvMeasurementStep1 => '1、戴紧AppleWatch,坐下来,保持心境平和'; | 430 | String get todayHrvMeasurementStep1 => '1、戴紧AppleWatch,坐下来,保持心境平和'; |
| @@ -426,7 +442,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -426,7 +442,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 426 | String get todayHrvMeasurementStep5 => '5、等待一分钟左右,StressWatch会收到你的数据并展示'; | 442 | String get todayHrvMeasurementStep5 => '5、等待一分钟左右,StressWatch会收到你的数据并展示'; |
| 427 | 443 | ||
| 428 | @override | 444 | @override |
| 429 | - String get todayHrvMeasurementHint => '提示:数据源来自AppleWatch,在测量之后可能存在延迟或是数据未能同步的情况。如若出现上述情况,请重新测量并等待数据读取。'; | 445 | + String get todayHrvMeasurementHint => |
| 446 | + '提示:数据源来自AppleWatch,在测量之后可能存在延迟或是数据未能同步的情况。如若出现上述情况,请重新测量并等待数据读取。'; | ||
| 430 | 447 | ||
| 431 | @override | 448 | @override |
| 432 | String get todayHrvMeasurementWarning => '注意:需打开健康里的权限,同时关闭省电模式。'; | 449 | String get todayHrvMeasurementWarning => '注意:需打开健康里的权限,同时关闭省电模式。'; |
| @@ -435,10 +452,12 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -435,10 +452,12 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 435 | String get todayStressStatusWhatTitle => '什么是综合压力状态?'; | 452 | String get todayStressStatusWhatTitle => '什么是综合压力状态?'; |
| 436 | 453 | ||
| 437 | @override | 454 | @override |
| 438 | - String get todayStressStatusWhatDescription1 => 'DoubleFeel 会结合你过去 30 天的 HRV(心率变异性)、静息心率以及当天的身体状态变化,综合评估你的整体压力水平。'; | 455 | + String get todayStressStatusWhatDescription1 => |
| 456 | + 'DoubleFeel 会结合你过去 30 天的 HRV(心率变异性)、静息心率以及当天的身体状态变化,综合评估你的整体压力水平。'; | ||
| 439 | 457 | ||
| 440 | @override | 458 | @override |
| 441 | - String get todayStressStatusWhatDescription2 => '由于 HRV 会随着情绪、运动、睡眠和疲劳不断波动,单次数据参考意义有限,因此我们更建议关注一整天的综合压力状态,让结果更稳定、更有参考价值。综合压力不仅能帮助你了解自己的身体状态,也能让亲密联系人更及时地关注你的变化。'; | 459 | + String get todayStressStatusWhatDescription2 => |
| 460 | + '由于 HRV 会随着情绪、运动、睡眠和疲劳不断波动,单次数据参考意义有限,因此我们更建议关注一整天的综合压力状态,让结果更稳定、更有参考价值。综合压力不仅能帮助你了解自己的身体状态,也能让亲密联系人更及时地关注你的变化。'; | ||
| 442 | 461 | ||
| 443 | @override | 462 | @override |
| 444 | String get todayStressStatusWhyHrvTitle => '为什么要参考 HRV(心率变异性)?'; | 463 | String get todayStressStatusWhyHrvTitle => '为什么要参考 HRV(心率变异性)?'; |
| @@ -459,22 +478,27 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -459,22 +478,27 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 459 | String get todayStressStatusHrvChangesFast => '· HRV 变化较快,更适合观察短时间内的身体状态变化。'; | 478 | String get todayStressStatusHrvChangesFast => '· HRV 变化较快,更适合观察短时间内的身体状态变化。'; |
| 460 | 479 | ||
| 461 | @override | 480 | @override |
| 462 | - String get todayStressStatusAppWatchDifferenceTitle => '手机 App 与 Apple Watch 显示的压力状态有什么区别?'; | 481 | + String get todayStressStatusAppWatchDifferenceTitle => |
| 482 | + '手机 App 与 Apple Watch 显示的压力状态有什么区别?'; | ||
| 463 | 483 | ||
| 464 | @override | 484 | @override |
| 465 | - String get todayStressStatusAppWatchDifferenceApp => '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整体趋势。'; | 485 | + String get todayStressStatusAppWatchDifferenceApp => |
| 486 | + '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整体趋势。'; | ||
| 466 | 487 | ||
| 467 | @override | 488 | @override |
| 468 | - String get todayStressStatusAppWatchDifferenceWatch => 'Apple Watch 显示的是最近一次的实时压力状态,更适合快速查看当前身体变化。'; | 489 | + String get todayStressStatusAppWatchDifferenceWatch => |
| 490 | + 'Apple Watch 显示的是最近一次的实时压力状态,更适合快速查看当前身体变化。'; | ||
| 469 | 491 | ||
| 470 | @override | 492 | @override |
| 471 | String get todayStressStatusWaitingDataTitle => '为什么会出现“等待数据”?'; | 493 | String get todayStressStatusWaitingDataTitle => '为什么会出现“等待数据”?'; |
| 472 | 494 | ||
| 473 | @override | 495 | @override |
| 474 | - String get todayStressStatusWaitingDataDescription1 => '“等待数据”代表当前采集到的数据量不足,暂时无法生成可靠的压力评估。'; | 496 | + String get todayStressStatusWaitingDataDescription1 => |
| 497 | + '“等待数据”代表当前采集到的数据量不足,暂时无法生成可靠的压力评估。'; | ||
| 475 | 498 | ||
| 476 | @override | 499 | @override |
| 477 | - String get todayStressStatusWaitingDataDescription2 => '请继续佩戴 Apple Watch,等待系统自动采集数据。'; | 500 | + String get todayStressStatusWaitingDataDescription2 => |
| 501 | + '请继续佩戴 Apple Watch,等待系统自动采集数据。'; | ||
| 478 | 502 | ||
| 479 | @override | 503 | @override |
| 480 | String get todayStressStatusWaitingDataReasonsIntro => '可能原因包括:'; | 504 | String get todayStressStatusWaitingDataReasonsIntro => '可能原因包括:'; |
| @@ -495,28 +519,35 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -495,28 +519,35 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 495 | String get todayHrvPrincipleHowMeasureTitle => 'DoubleFeel 如何测量压力状态?'; | 519 | String get todayHrvPrincipleHowMeasureTitle => 'DoubleFeel 如何测量压力状态?'; |
| 496 | 520 | ||
| 497 | @override | 521 | @override |
| 498 | - String get todayHrvPrincipleHowMeasureDescription1 => '当你正常佩戴 Apple Watch 时,系统会自动采集你的心率数据,并同步至 Apple Health。'; | 522 | + String get todayHrvPrincipleHowMeasureDescription1 => |
| 523 | + '当你正常佩戴 Apple Watch 时,系统会自动采集你的心率数据,并同步至 Apple Health。'; | ||
| 499 | 524 | ||
| 500 | @override | 525 | @override |
| 501 | - String get todayHrvPrincipleHowMeasureDescription2 => 'DoubleFeel 会基于这些数据计算 HRV(心率变异性)相关指标,用于评估你的身体压力与恢复状态。'; | 526 | + String get todayHrvPrincipleHowMeasureDescription2 => |
| 527 | + 'DoubleFeel 会基于这些数据计算 HRV(心率变异性)相关指标,用于评估你的身体压力与恢复状态。'; | ||
| 502 | 528 | ||
| 503 | @override | 529 | @override |
| 504 | - String get todayHrvPrincipleHowMeasureDescription3 => 'HRV 对压力、疲劳、睡眠、情绪与身体恢复都非常敏感,因此它能够帮助我们更早发现身体状态变化。'; | 530 | + String get todayHrvPrincipleHowMeasureDescription3 => |
| 531 | + 'HRV 对压力、疲劳、睡眠、情绪与身体恢复都非常敏感,因此它能够帮助我们更早发现身体状态变化。'; | ||
| 505 | 532 | ||
| 506 | @override | 533 | @override |
| 507 | - String get todayHrvPrincipleHowMeasureDescription4 => '为了让结果更准确,DoubleFeel 会将你当前的 HRV 状态与过去 30 天的个人平均水平进行对比,而不是直接与其他人比较。'; | 534 | + String get todayHrvPrincipleHowMeasureDescription4 => |
| 535 | + '为了让结果更准确,DoubleFeel 会将你当前的 HRV 状态与过去 30 天的个人平均水平进行对比,而不是直接与其他人比较。'; | ||
| 508 | 536 | ||
| 509 | @override | 537 | @override |
| 510 | String get todayRealtimeStressWhatTitle => '什么是实时压力?'; | 538 | String get todayRealtimeStressWhatTitle => '什么是实时压力?'; |
| 511 | 539 | ||
| 512 | @override | 540 | @override |
| 513 | - String get todayRealtimeStressWhatDescription1 => '实时压力是 DoubleFeel 根据你当前的 HRV、心率状态与个人历史数据变化,动态生成的身体压力指标。'; | 541 | + String get todayRealtimeStressWhatDescription1 => |
| 542 | + '实时压力是 DoubleFeel 根据你当前的 HRV、心率状态与个人历史数据变化,动态生成的身体压力指标。'; | ||
| 514 | 543 | ||
| 515 | @override | 544 | @override |
| 516 | - String get todayRealtimeStressWhatDescription2 => '压力值越高,代表你的身体状态相比平时偏离越明显,可能正处于疲劳、恢复不足或高压力状态。'; | 545 | + String get todayRealtimeStressWhatDescription2 => |
| 546 | + '压力值越高,代表你的身体状态相比平时偏离越明显,可能正处于疲劳、恢复不足或高压力状态。'; | ||
| 517 | 547 | ||
| 518 | @override | 548 | @override |
| 519 | - String get todayRealtimeStressWhatDescription3 => '它能够帮助你更快发现身体变化,并及时调整休息、运动与生活节奏。'; | 549 | + String get todayRealtimeStressWhatDescription3 => |
| 550 | + '它能够帮助你更快发现身体变化,并及时调整休息、运动与生活节奏。'; | ||
| 520 | 551 | ||
| 521 | @override | 552 | @override |
| 522 | String get todayRealtimeStressDivisionTitle => '实时压力如何划分?'; | 553 | String get todayRealtimeStressDivisionTitle => '实时压力如何划分?'; |
| @@ -546,10 +577,12 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -546,10 +577,12 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 546 | String get todayRealtimeStressCautionDescription => '身体可能正在积累压力,需要适当休息与恢复。'; | 577 | String get todayRealtimeStressCautionDescription => '身体可能正在积累压力,需要适当休息与恢复。'; |
| 547 | 578 | ||
| 548 | @override | 579 | @override |
| 549 | - String get todayRealtimeStressOverloadDescription => '身体压力明显偏高,建议减少负荷、注意睡眠与恢复。'; | 580 | + String get todayRealtimeStressOverloadDescription => |
| 581 | + '身体压力明显偏高,建议减少负荷、注意睡眠与恢复。'; | ||
| 550 | 582 | ||
| 551 | @override | 583 | @override |
| 552 | - String get todayRealtimeStressDivisionBaseline => '以上区间会结合你的个人基线动态调整,不同用户之间并不直接比较。'; | 584 | + String get todayRealtimeStressDivisionBaseline => |
| 585 | + '以上区间会结合你的个人基线动态调整,不同用户之间并不直接比较。'; | ||
| 553 | 586 | ||
| 554 | @override | 587 | @override |
| 555 | String get todayRealtimeStressDivisionAwake => '此外,实时压力主要反映清醒状态下的身体压力变化。'; | 588 | String get todayRealtimeStressDivisionAwake => '此外,实时压力主要反映清醒状态下的身体压力变化。'; |
| @@ -564,25 +597,31 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -564,25 +597,31 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 564 | String get todayRealtimeStressLowBetterType => '身体压力分为“正常压力”与“异常压力”。'; | 597 | String get todayRealtimeStressLowBetterType => '身体压力分为“正常压力”与“异常压力”。'; |
| 565 | 598 | ||
| 566 | @override | 599 | @override |
| 567 | - String get todayRealtimeStressLowBetterExample => '例如:运动期间或运动后,实时压力短时间升高属于正常恢复反应;工作专注、情绪兴奋时,压力也可能暂时升高,这些都属于正常的身体调节。'; | 600 | + String get todayRealtimeStressLowBetterExample => |
| 601 | + '例如:运动期间或运动后,实时压力短时间升高属于正常恢复反应;工作专注、情绪兴奋时,压力也可能暂时升高,这些都属于正常的身体调节。'; | ||
| 568 | 602 | ||
| 569 | @override | 603 | @override |
| 570 | - String get todayRealtimeStressLowBetterHighStress => '但如果在静息、久坐或睡眠不足的情况下,压力长期偏高,则可能意味着身体疲劳、心理压力较大、睡眠恢复不足、运动恢复不充分、摄入过多咖啡因、酒精或刺激物、身体可能处于不适状态。'; | 604 | + String get todayRealtimeStressLowBetterHighStress => |
| 605 | + '但如果在静息、久坐或睡眠不足的情况下,压力长期偏高,则可能意味着身体疲劳、心理压力较大、睡眠恢复不足、运动恢复不充分、摄入过多咖啡因、酒精或刺激物、身体可能处于不适状态。'; | ||
| 571 | 606 | ||
| 572 | @override | 607 | @override |
| 573 | - String get todayRealtimeStressLowBetterTrend => 'DoubleFeel 更关注的是你的长期变化趋势,而不是单次波动。'; | 608 | + String get todayRealtimeStressLowBetterTrend => |
| 609 | + 'DoubleFeel 更关注的是你的长期变化趋势,而不是单次波动。'; | ||
| 574 | 610 | ||
| 575 | @override | 611 | @override |
| 576 | String get todayRealtimeStressScenarioTitle => 'HRV 与实时压力适用场景?'; | 612 | String get todayRealtimeStressScenarioTitle => 'HRV 与实时压力适用场景?'; |
| 577 | 613 | ||
| 578 | @override | 614 | @override |
| 579 | - String get todayRealtimeStressScenarioHrvDefault => '在 Apple Watch 的默认设置下,HRV 每 2~5 小时更新一次。'; | 615 | + String get todayRealtimeStressScenarioHrvDefault => |
| 616 | + '在 Apple Watch 的默认设置下,HRV 每 2~5 小时更新一次。'; | ||
| 580 | 617 | ||
| 581 | @override | 618 | @override |
| 582 | - String get todayRealtimeStressScenarioRegionLimit => '在部分地区,由于 Apple Watch 的呼吸功能受限,HRV 的更新频率可能会受到影响,并且开启呼吸功能后也会消耗更多电量。'; | 619 | + String get todayRealtimeStressScenarioRegionLimit => |
| 620 | + '在部分地区,由于 Apple Watch 的呼吸功能受限,HRV 的更新频率可能会受到影响,并且开启呼吸功能后也会消耗更多电量。'; | ||
| 583 | 621 | ||
| 584 | @override | 622 | @override |
| 585 | - String get todayRealtimeStressScenarioIntro => '为了解决 HRV 更新间隔较长的问题,DoubleFeel 设计了实时压力功能:'; | 623 | + String get todayRealtimeStressScenarioIntro => |
| 624 | + '为了解决 HRV 更新间隔较长的问题,DoubleFeel 设计了实时压力功能:'; | ||
| 586 | 625 | ||
| 587 | @override | 626 | @override |
| 588 | String get todayRealtimeStressScenarioUpdateEvery6Min => '· 实时压力每 6 分钟更新一次'; | 627 | String get todayRealtimeStressScenarioUpdateEvery6Min => '· 实时压力每 6 分钟更新一次'; |
| @@ -591,22 +630,27 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -591,22 +630,27 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 591 | String get todayRealtimeStressScenarioTimely => '· 可以更及时地反映身体状态变化'; | 630 | String get todayRealtimeStressScenarioTimely => '· 可以更及时地反映身体状态变化'; |
| 592 | 631 | ||
| 593 | @override | 632 | @override |
| 594 | - String get todayRealtimeStressScenarioConsistentTrend => '· 在大多数情况下,实时压力趋势与 HRV 趋势是一致的'; | 633 | + String get todayRealtimeStressScenarioConsistentTrend => |
| 634 | + '· 在大多数情况下,实时压力趋势与 HRV 趋势是一致的'; | ||
| 595 | 635 | ||
| 596 | @override | 636 | @override |
| 597 | - String get todayRealtimeStressScenarioSummary => '这样用户既能获得 HRV 的长期趋势,也能通过实时压力获得短时身体状态的参考。'; | 637 | + String get todayRealtimeStressScenarioSummary => |
| 638 | + '这样用户既能获得 HRV 的长期趋势,也能通过实时压力获得短时身体状态的参考。'; | ||
| 598 | 639 | ||
| 599 | @override | 640 | @override |
| 600 | String get todayFaqNoDataTitle => 'APP或表盘有没有数据怎么办?'; | 641 | String get todayFaqNoDataTitle => 'APP或表盘有没有数据怎么办?'; |
| 601 | 642 | ||
| 602 | @override | 643 | @override |
| 603 | - String get todayFaqNoDataDescription1 => '1. 确认苹果手表系统在10.0以上,手机系统在14以上,系统版本可在「关于本机」内查看。'; | 644 | + String get todayFaqNoDataDescription1 => |
| 645 | + '1. 确认苹果手表系统在10.0以上,手机系统在14以上,系统版本可在「关于本机」内查看。'; | ||
| 604 | 646 | ||
| 605 | @override | 647 | @override |
| 606 | - String get todayFaqNoDataDescription2 => '2. 确认是否开启所有权限:手机「健康」-「共享」-「app」-「DoubleFeel」-「打开所有权限」。'; | 648 | + String get todayFaqNoDataDescription2 => |
| 649 | + '2. 确认是否开启所有权限:手机「健康」-「共享」-「app」-「DoubleFeel」-「打开所有权限」。'; | ||
| 607 | 650 | ||
| 608 | @override | 651 | @override |
| 609 | - String get todayFaqNoDataDescription3 => '3. 确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。'; | 652 | + String get todayFaqNoDataDescription3 => |
| 653 | + '3. 确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。'; | ||
| 610 | 654 | ||
| 611 | @override | 655 | @override |
| 612 | String get todayFaqContactPrefix => '如以上均检查无问题,可以'; | 656 | String get todayFaqContactPrefix => '如以上均检查无问题,可以'; |
| @@ -621,37 +665,46 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -621,37 +665,46 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 621 | String get todayFaqWatchNoNotificationTitle => '手表无法收到状态通知?'; | 665 | String get todayFaqWatchNoNotificationTitle => '手表无法收到状态通知?'; |
| 622 | 666 | ||
| 623 | @override | 667 | @override |
| 624 | - String get todayFaqWatchNoNotificationDescription1 => '苹果手表和手机的通知展示有优先级:当手机已解锁并亮屏时,通知只会在手机端展示,不会在手表上出现。'; | 668 | + String get todayFaqWatchNoNotificationDescription1 => |
| 669 | + '苹果手表和手机的通知展示有优先级:当手机已解锁并亮屏时,通知只会在手机端展示,不会在手表上出现。'; | ||
| 625 | 670 | ||
| 626 | @override | 671 | @override |
| 627 | - String get todayFaqWatchNoNotificationDescription2 => '若压力数据可正常显示和自动更新,但手表未收到通知,可尝试以下操作:'; | 672 | + String get todayFaqWatchNoNotificationDescription2 => |
| 673 | + '若压力数据可正常显示和自动更新,但手表未收到通知,可尝试以下操作:'; | ||
| 628 | 674 | ||
| 629 | @override | 675 | @override |
| 630 | - String get todayFaqWatchNoNotificationCheckPhoneNotification => '1. 检查手机是否打开通知(设置-DoubleFeel-通知)。'; | 676 | + String get todayFaqWatchNoNotificationCheckPhoneNotification => |
| 677 | + '1. 检查手机是否打开通知(设置-DoubleFeel-通知)。'; | ||
| 631 | 678 | ||
| 632 | @override | 679 | @override |
| 633 | - String get todayFaqWatchNoNotificationCheckPhoneBackgroundRefresh => '2. 检查手机是否打开后台App刷新(设置-DoubleFeel-后台App刷新)。'; | 680 | + String get todayFaqWatchNoNotificationCheckPhoneBackgroundRefresh => |
| 681 | + '2. 检查手机是否打开后台App刷新(设置-DoubleFeel-后台App刷新)。'; | ||
| 634 | 682 | ||
| 635 | @override | 683 | @override |
| 636 | - String get todayFaqWatchNoNotificationCheckWatchBackgroundRefresh => '3. 检查手表是否打开后台App刷新(设置-通用-后台App刷新,并确保DoubleFeel开启)。'; | 684 | + String get todayFaqWatchNoNotificationCheckWatchBackgroundRefresh => |
| 685 | + '3. 检查手表是否打开后台App刷新(设置-通用-后台App刷新,并确保DoubleFeel开启)。'; | ||
| 637 | 686 | ||
| 638 | @override | 687 | @override |
| 639 | - String get todayFaqWatchNoNotificationCheckModes => '4. 确保未处于低电量/专注/勿扰/剧院/睡眠等模式。'; | 688 | + String get todayFaqWatchNoNotificationCheckModes => |
| 689 | + '4. 确保未处于低电量/专注/勿扰/剧院/睡眠等模式。'; | ||
| 640 | 690 | ||
| 641 | @override | 691 | @override |
| 642 | - String get todayFaqWatchNoNotificationReinstall => '5. 重装DoubleFeel 并重启AppleWatch与iPhone。'; | 692 | + String get todayFaqWatchNoNotificationReinstall => |
| 693 | + '5. 重装DoubleFeel 并重启AppleWatch与iPhone。'; | ||
| 643 | 694 | ||
| 644 | @override | 695 | @override |
| 645 | String get todayFaqWatchFaceDelayTitle => '手表表盘数据不更新或有延迟?'; | 696 | String get todayFaqWatchFaceDelayTitle => '手表表盘数据不更新或有延迟?'; |
| 646 | 697 | ||
| 647 | @override | 698 | @override |
| 648 | - String get todayFaqWatchFaceDelayDescription1 => '由于苹果系统限制,所有手表表盘(第三方或官方)都会存在几分钟至半小时的延迟,开发者无法控制刷新频率。'; | 699 | + String get todayFaqWatchFaceDelayDescription1 => |
| 700 | + '由于苹果系统限制,所有手表表盘(第三方或官方)都会存在几分钟至半小时的延迟,开发者无法控制刷新频率。'; | ||
| 649 | 701 | ||
| 650 | @override | 702 | @override |
| 651 | String get todayFaqWatchFaceDelayIfOverOneHour => '若手机数据刷新后超过1小时表盘仍未更新:'; | 703 | String get todayFaqWatchFaceDelayIfOverOneHour => '若手机数据刷新后超过1小时表盘仍未更新:'; |
| 652 | 704 | ||
| 653 | @override | 705 | @override |
| 654 | - String get todayFaqWatchFaceDelayOpenWatchApp => '请在手表上手动打开DoubleFeel,等待约1分钟。'; | 706 | + String get todayFaqWatchFaceDelayOpenWatchApp => |
| 707 | + '请在手表上手动打开DoubleFeel,等待约1分钟。'; | ||
| 655 | 708 | ||
| 656 | @override | 709 | @override |
| 657 | String get todayFaqWatchFaceDelayIfStill => '若仍未更新:'; | 710 | String get todayFaqWatchFaceDelayIfStill => '若仍未更新:'; |
| @@ -666,22 +719,27 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -666,22 +719,27 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 666 | String get todayFaqWatchFaceDelayCheckData => '· 手机和手表app是否可正常看到HRV数据。'; | 719 | String get todayFaqWatchFaceDelayCheckData => '· 手机和手表app是否可正常看到HRV数据。'; |
| 667 | 720 | ||
| 668 | @override | 721 | @override |
| 669 | - String get todayFaqWatchFaceDelayCheckPhoneHealth => '· 确保手机端「iOS设置-隐私与安全性-健康-DoubleFeel」全部授权。'; | 722 | + String get todayFaqWatchFaceDelayCheckPhoneHealth => |
| 723 | + '· 确保手机端「iOS设置-隐私与安全性-健康-DoubleFeel」全部授权。'; | ||
| 670 | 724 | ||
| 671 | @override | 725 | @override |
| 672 | - String get todayFaqWatchFaceDelayCheckWatchHealth => '· 确保手表端「设置-健康-数据来源、App和服务-DoubleFeel」全部授权。'; | 726 | + String get todayFaqWatchFaceDelayCheckWatchHealth => |
| 727 | + '· 确保手表端「设置-健康-数据来源、App和服务-DoubleFeel」全部授权。'; | ||
| 673 | 728 | ||
| 674 | @override | 729 | @override |
| 675 | - String get todayFaqWatchFaceDelayCheckBackgroundRefresh => '· 确认AppleWatch-设置-通用-后台App刷新中DoubleFeel已开启。'; | 730 | + String get todayFaqWatchFaceDelayCheckBackgroundRefresh => |
| 731 | + '· 确认AppleWatch-设置-通用-后台App刷新中DoubleFeel已开启。'; | ||
| 676 | 732 | ||
| 677 | @override | 733 | @override |
| 678 | - String get todayFaqWatchFaceDelayRestartWatch => '· 若仍未自动刷新,请重启手表。长时间运行或后台占用过高可能导致表盘暂停更新。'; | 734 | + String get todayFaqWatchFaceDelayRestartWatch => |
| 735 | + '· 若仍未自动刷新,请重启手表。长时间运行或后台占用过高可能导致表盘暂停更新。'; | ||
| 679 | 736 | ||
| 680 | @override | 737 | @override |
| 681 | String get todayFaqWatchFaceBlackScreenTitle => '手表表盘出现黑屏?'; | 738 | String get todayFaqWatchFaceBlackScreenTitle => '手表表盘出现黑屏?'; |
| 682 | 739 | ||
| 683 | @override | 740 | @override |
| 684 | - String get todayFaqWatchFaceBlackScreenDescription => '若添加专属互动表盘后出现黑屏(仅显示时间和日期),可长按表盘,点击「编辑」,左滑至「复杂功能」,选择 DoubleFeel,然后按需选择各组件重新添加。'; | 741 | + String get todayFaqWatchFaceBlackScreenDescription => |
| 742 | + '若添加专属互动表盘后出现黑屏(仅显示时间和日期),可长按表盘,点击「编辑」,左滑至「复杂功能」,选择 DoubleFeel,然后按需选择各组件重新添加。'; | ||
| 685 | 743 | ||
| 686 | @override | 744 | @override |
| 687 | String get today => '今天'; | 745 | String get today => '今天'; |
| @@ -765,7 +823,8 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -765,7 +823,8 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 765 | String get questionsAndFeedback => '问题和反馈'; | 823 | String get questionsAndFeedback => '问题和反馈'; |
| 766 | 824 | ||
| 767 | @override | 825 | @override |
| 768 | - String get ifYouWouldLikeUsToReplyPleaseProvideYourEmailAddress => '如果需要我们回复,请填写联系邮箱'; | 826 | + String get ifYouWouldLikeUsToReplyPleaseProvideYourEmailAddress => |
| 827 | + '如果需要我们回复,请填写联系邮箱'; | ||
| 769 | 828 | ||
| 770 | @override | 829 | @override |
| 771 | String get uploadProof => '上传凭证'; | 830 | String get uploadProof => '上传凭证'; |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/foundation.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 2 | import 'package:flutter/services.dart'; | 5 | import 'package:flutter/services.dart'; |
| 3 | import 'package:intl/date_symbol_data_local.dart'; | 6 | import 'package:intl/date_symbol_data_local.dart'; |
| @@ -7,11 +10,22 @@ import 'app/double_feel_app.dart'; | @@ -7,11 +10,22 @@ import 'app/double_feel_app.dart'; | ||
| 7 | import 'core/theme/app_theme.dart'; | 10 | import 'core/theme/app_theme.dart'; |
| 8 | 11 | ||
| 9 | Future<void> main() async { | 12 | Future<void> main() async { |
| 10 | - WidgetsFlutterBinding.ensureInitialized(); | 13 | + runZonedGuarded<Future<Null>>( |
| 14 | + () async { | ||
| 15 | + WidgetsFlutterBinding.ensureInitialized(); | ||
| 16 | + FlutterError.onError = (errorDetails) {}; | ||
| 17 | + PlatformDispatcher.instance.onError = (error, stack) { | ||
| 18 | + return true; | ||
| 19 | + }; | ||
| 11 | 20 | ||
| 12 | - SystemChrome.setSystemUIOverlayStyle(AppTheme.systemUiOverlayStyle); | 21 | + SystemChrome.setSystemUIOverlayStyle(AppTheme.systemUiOverlayStyle); |
| 13 | 22 | ||
| 14 | - await AppBootstrap.init(); | ||
| 15 | - await initializeDateFormatting(); | ||
| 16 | - runApp(const DoubleFeelApp()); | 23 | + await AppBootstrap.init(); |
| 24 | + await initializeDateFormatting(); | ||
| 25 | + runApp(const DoubleFeelApp()); | ||
| 26 | + }, | ||
| 27 | + (error, stackTrace) async { | ||
| 28 | + if (kDebugMode) {} | ||
| 29 | + }, | ||
| 30 | + ); | ||
| 17 | } | 31 | } |
| @@ -70,5 +70,5 @@ flutter: | @@ -70,5 +70,5 @@ flutter: | ||
| 70 | - assets/images/today/ | 70 | - assets/images/today/ |
| 71 | - assets/images/friends/ | 71 | - assets/images/friends/ |
| 72 | - assets/images/my/ | 72 | - assets/images/my/ |
| 73 | - - assets/images/my/ | 73 | + - assets/images/permission/ |
| 74 | - assets/images/watch_theme/ | 74 | - assets/images/watch_theme/ |
-
Please register or login to post a comment