Commit 3321b51a7ba1a7d03f9c3adfb0a3aa3b1cbd50d7

Authored by 权海
1 parent 7b5acdb7

feat(ui):增加我的-watch主题

Showing 53 changed files with 3221 additions and 80 deletions
@@ -8,8 +8,7 @@ @@ -8,8 +8,7 @@
8 "name": "Flutter", 8 "name": "Flutter",
9 "type": "dart", 9 "type": "dart",
10 "request": "launch", 10 "request": "launch",
11 - "program": "lib/main.dart",  
12 - "flutterMode": "profile" 11 + "program": "lib/main.dart"
13 }, 12 },
14 ] 13 ]
15 } 14 }
@@ -172,7 +172,6 @@ @@ -172,7 +172,6 @@
172 2ECF3C9772810B1582D524F4 /* Pods-RunnerTests.release.xcconfig */, 172 2ECF3C9772810B1582D524F4 /* Pods-RunnerTests.release.xcconfig */,
173 5620C49637B3BA24A7B2A069 /* Pods-RunnerTests.profile.xcconfig */, 173 5620C49637B3BA24A7B2A069 /* Pods-RunnerTests.profile.xcconfig */,
174 ); 174 );
175 - name = Pods;  
176 path = Pods; 175 path = Pods;
177 sourceTree = "<group>"; 176 sourceTree = "<group>";
178 }; 177 };
@@ -524,7 +523,7 @@ @@ -524,7 +523,7 @@
524 "$(inherited)", 523 "$(inherited)",
525 "@executable_path/Frameworks", 524 "@executable_path/Frameworks",
526 ); 525 );
527 - PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel; 526 + PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel.flutter;
528 PRODUCT_NAME = "$(TARGET_NAME)"; 527 PRODUCT_NAME = "$(TARGET_NAME)";
529 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 528 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
530 SWIFT_VERSION = 5.0; 529 SWIFT_VERSION = 5.0;
@@ -707,7 +706,7 @@ @@ -707,7 +706,7 @@
707 "$(inherited)", 706 "$(inherited)",
708 "@executable_path/Frameworks", 707 "@executable_path/Frameworks",
709 ); 708 );
710 - PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel; 709 + PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel.flutter;
711 PRODUCT_NAME = "$(TARGET_NAME)"; 710 PRODUCT_NAME = "$(TARGET_NAME)";
712 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 711 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
713 SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 712 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -730,7 +729,7 @@ @@ -730,7 +729,7 @@
730 "$(inherited)", 729 "$(inherited)",
731 "@executable_path/Frameworks", 730 "@executable_path/Frameworks",
732 ); 731 );
733 - PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel; 732 + PRODUCT_BUNDLE_IDENTIFIER = com.luiz.doublefeel.flutter;
734 PRODUCT_NAME = "$(TARGET_NAME)"; 733 PRODUCT_NAME = "$(TARGET_NAME)";
735 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 734 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
736 SWIFT_VERSION = 5.0; 735 SWIFT_VERSION = 5.0;
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0"> 3 <plist version="1.0">
4 <dict> 4 <dict>
  5 + <key>CADisableMinimumFrameDurationOnPhone</key>
  6 + <true/>
5 <key>CFBundleDevelopmentRegion</key> 7 <key>CFBundleDevelopmentRegion</key>
6 <string>$(DEVELOPMENT_LANGUAGE)</string> 8 <string>$(DEVELOPMENT_LANGUAGE)</string>
7 <key>CFBundleDisplayName</key> 9 <key>CFBundleDisplayName</key>
@@ -24,6 +26,10 @@ @@ -24,6 +26,10 @@
24 <string>$(FLUTTER_BUILD_NUMBER)</string> 26 <string>$(FLUTTER_BUILD_NUMBER)</string>
25 <key>LSRequiresIPhoneOS</key> 27 <key>LSRequiresIPhoneOS</key>
26 <true/> 28 <true/>
  29 + <key>NSPhotoLibraryUsageDescription</key>
  30 + <string>用于选择图片并生成自定义 Watch 表盘主题。</string>
  31 + <key>UIApplicationSupportsIndirectInputEvents</key>
  32 + <true/>
27 <key>UILaunchStoryboardName</key> 33 <key>UILaunchStoryboardName</key>
28 <string>LaunchScreen</string> 34 <string>LaunchScreen</string>
29 <key>UIMainStoryboardFile</key> 35 <key>UIMainStoryboardFile</key>
@@ -41,9 +47,5 @@ @@ -41,9 +47,5 @@
41 <string>UIInterfaceOrientationLandscapeLeft</string> 47 <string>UIInterfaceOrientationLandscapeLeft</string>
42 <string>UIInterfaceOrientationLandscapeRight</string> 48 <string>UIInterfaceOrientationLandscapeRight</string>
43 </array> 49 </array>
44 - <key>CADisableMinimumFrameDurationOnPhone</key>  
45 - <true/>  
46 - <key>UIApplicationSupportsIndirectInputEvents</key>  
47 - <true/>  
48 </dict> 50 </dict>
49 </plist> 51 </plist>
1 -import Foundation  
2 import Flutter 1 import Flutter
  2 +import Foundation
  3 +import PhotosUI
  4 +import UIKit
  5 +import Vision
3 6
4 private let unsupported = "HealthKit / WearEngine / Alipay are not supported on iOS in this build." 7 private let unsupported = "HealthKit / WearEngine / Alipay are not supported on iOS in this build."
5 8
@@ -19,6 +22,12 @@ final class WearEngineHostApiStub: WearEngineHostApi { @@ -19,6 +22,12 @@ final class WearEngineHostApiStub: WearEngineHostApi {
19 func registerMessageReceiver() throws -> Bool { false } 22 func registerMessageReceiver() throws -> Bool { false }
20 func sendTextMessage(message: String) throws -> Bool { false } 23 func sendTextMessage(message: String) throws -> Bool { false }
21 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool { false } 24 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool { false }
  25 + func pickImageAndRemoveBackground() throws -> String? {
  26 + if #available(iOS 14.0, *) {
  27 + return WatchThemeImagePicker().pickImageAndRemoveBackground()
  28 + }
  29 + return nil
  30 + }
22 } 31 }
23 32
24 final class AlipayHostApiStub: AlipayHostApi { 33 final class AlipayHostApiStub: AlipayHostApi {
@@ -32,3 +41,197 @@ enum NativePigeonRegistrar { @@ -32,3 +41,197 @@ enum NativePigeonRegistrar {
32 AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub()) 41 AlipayHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: AlipayHostApiStub())
33 } 42 }
34 } 43 }
  44 +
  45 +@available(iOS 14.0, *)
  46 +private final class WatchThemeImagePicker: NSObject, PHPickerViewControllerDelegate {
  47 + private var continuation: CheckedContinuation<String?, Never>?
  48 +
  49 + func pickImageAndRemoveBackground() -> String? {
  50 + if Thread.isMainThread {
  51 + return runOnMain()
  52 + }
  53 +
  54 + var result: String?
  55 + let semaphore = DispatchSemaphore(value: 0)
  56 + DispatchQueue.main.async {
  57 + result = self.runOnMain()
  58 + semaphore.signal()
  59 + }
  60 + semaphore.wait()
  61 + return result
  62 + }
  63 +
  64 + private func runOnMain() -> String? {
  65 + var configuration = PHPickerConfiguration(photoLibrary: .shared())
  66 + configuration.filter = .images
  67 + configuration.selectionLimit = 1
  68 +
  69 + guard let presenter = UIApplication.shared.topMostViewController else {
  70 + return nil
  71 + }
  72 +
  73 + let picker = PHPickerViewController(configuration: configuration)
  74 + picker.delegate = self
  75 +
  76 + return waitForPickerResult(picker: picker, presenter: presenter)
  77 + }
  78 +
  79 + private func waitForPickerResult(
  80 + picker: PHPickerViewController,
  81 + presenter: UIViewController
  82 + ) -> String? {
  83 + var pickedPath: String?
  84 + let semaphore = DispatchSemaphore(value: 0)
  85 +
  86 + Task { @MainActor in
  87 + pickedPath = await withCheckedContinuation { continuation in
  88 + self.continuation = continuation
  89 + presenter.present(picker, animated: true)
  90 + }
  91 + semaphore.signal()
  92 + }
  93 +
  94 + while semaphore.wait(timeout: .now() + 0.05) == .timedOut {
  95 + RunLoop.main.run(mode: .default, before: Date(timeIntervalSinceNow: 0.05))
  96 + }
  97 + return pickedPath
  98 + }
  99 +
  100 + nonisolated func picker(
  101 + _ picker: PHPickerViewController,
  102 + didFinishPicking results: [PHPickerResult]
  103 + ) {
  104 + Task { @MainActor in
  105 + picker.dismiss(animated: true)
  106 +
  107 + guard let provider = results.first?.itemProvider,
  108 + provider.canLoadObject(ofClass: UIImage.self) else {
  109 + continuation?.resume(returning: nil)
  110 + continuation = nil
  111 + return
  112 + }
  113 +
  114 + provider.loadObject(ofClass: UIImage.self) { object, _ in
  115 + Task { @MainActor in
  116 + guard let image = object as? UIImage else {
  117 + self.continuation?.resume(returning: nil)
  118 + self.continuation = nil
  119 + return
  120 + }
  121 + let processed = await WatchThemeImageProcessor.removeBackground(from: image)
  122 + let path = WatchThemeImageProcessor.savePNG(processed)
  123 + self.continuation?.resume(returning: path)
  124 + self.continuation = nil
  125 + }
  126 + }
  127 + }
  128 + }
  129 +}
  130 +
  131 +private enum WatchThemeImageProcessor {
  132 + static func removeBackground(from image: UIImage) async -> UIImage {
  133 + guard #available(iOS 17.0, *),
  134 + let cgImage = image.normalizedCGImage else {
  135 + return image
  136 + }
  137 +
  138 + return await Task.detached(priority: .userInitiated) {
  139 + let request = VNGenerateForegroundInstanceMaskRequest()
  140 + let handler = VNImageRequestHandler(cgImage: cgImage)
  141 +
  142 + do {
  143 + try handler.perform([request])
  144 + guard let observation = request.results?.first else {
  145 + return image
  146 + }
  147 +
  148 + let mask = try observation.generateScaledMaskForImage(
  149 + forInstances: observation.allInstances,
  150 + from: handler
  151 + )
  152 + return composite(image: cgImage, mask: mask) ?? image
  153 + } catch {
  154 + return image
  155 + }
  156 + }.value
  157 + }
  158 +
  159 + static func savePNG(_ image: UIImage) -> String? {
  160 + guard let data = image.pngData() else { return nil }
  161 + let directory = FileManager.default.temporaryDirectory
  162 + .appendingPathComponent("watch_theme", isDirectory: true)
  163 +
  164 + do {
  165 + try FileManager.default.createDirectory(
  166 + at: directory,
  167 + withIntermediateDirectories: true
  168 + )
  169 + let file = directory.appendingPathComponent("\(UUID().uuidString).png")
  170 + try data.write(to: file, options: .atomic)
  171 + return file.path
  172 + } catch {
  173 + return nil
  174 + }
  175 + }
  176 +
  177 + private static func composite(image: CGImage, mask: CVPixelBuffer) -> UIImage? {
  178 + let ciImage = CIImage(cgImage: image)
  179 + let ciMask = CIImage(cvPixelBuffer: mask)
  180 + guard let filter = CIFilter(name: "CIBlendWithMask") else {
  181 + return nil
  182 + }
  183 + filter.setValue(ciImage, forKey: kCIInputImageKey)
  184 + filter.setValue(ciMask, forKey: kCIInputMaskImageKey)
  185 + filter.setValue(
  186 + CIImage(color: .clear).cropped(to: ciImage.extent),
  187 + forKey: kCIInputBackgroundImageKey
  188 + )
  189 +
  190 + guard let output = filter.outputImage,
  191 + let cgOutput = CIContext().createCGImage(output, from: ciImage.extent) else {
  192 + return nil
  193 + }
  194 + return UIImage(cgImage: cgOutput, scale: 1, orientation: .up)
  195 + }
  196 +}
  197 +
  198 +private extension UIImage {
  199 + var normalizedCGImage: CGImage? {
  200 + if imageOrientation == .up, let cgImage {
  201 + return cgImage
  202 + }
  203 +
  204 + let format = UIGraphicsImageRendererFormat.default()
  205 + format.scale = scale
  206 + let renderer = UIGraphicsImageRenderer(size: size, format: format)
  207 + return renderer.image { _ in
  208 + draw(in: CGRect(origin: .zero, size: size))
  209 + }.cgImage
  210 + }
  211 +}
  212 +
  213 +private extension UIApplication {
  214 + var topMostViewController: UIViewController? {
  215 + connectedScenes
  216 + .compactMap { $0 as? UIWindowScene }
  217 + .flatMap(\.windows)
  218 + .first { $0.isKeyWindow }?
  219 + .rootViewController?
  220 + .topMostPresented
  221 + }
  222 +}
  223 +
  224 +private extension UIViewController {
  225 + var topMostPresented: UIViewController {
  226 + if let presentedViewController {
  227 + return presentedViewController.topMostPresented
  228 + }
  229 + if let navigationController = self as? UINavigationController {
  230 + return navigationController.visibleViewController?.topMostPresented ?? navigationController
  231 + }
  232 + if let tabBarController = self as? UITabBarController {
  233 + return tabBarController.selectedViewController?.topMostPresented ?? tabBarController
  234 + }
  235 + return self
  236 + }
  237 +}
@@ -188,6 +188,7 @@ protocol WearEngineHostApi { @@ -188,6 +188,7 @@ protocol WearEngineHostApi {
188 func registerMessageReceiver() throws -> Bool 188 func registerMessageReceiver() throws -> Bool
189 func sendTextMessage(message: String) throws -> Bool 189 func sendTextMessage(message: String) throws -> Bool
190 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool 190 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool
  191 + func pickImageAndRemoveBackground() throws -> String?
191 } 192 }
192 193
193 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 194 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -265,5 +266,18 @@ class WearEngineHostApiSetup { @@ -265,5 +266,18 @@ class WearEngineHostApiSetup {
265 } else { 266 } else {
266 sendWatchSyncPayloadChannel.setMessageHandler(nil) 267 sendWatchSyncPayloadChannel.setMessageHandler(nil)
267 } 268 }
  269 + let pickImageAndRemoveBackgroundChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  270 + if let api = api {
  271 + pickImageAndRemoveBackgroundChannel.setMessageHandler { _, reply in
  272 + do {
  273 + let result = try api.pickImageAndRemoveBackground()
  274 + reply(wrapResult(result))
  275 + } catch {
  276 + reply(wrapError(error))
  277 + }
  278 + }
  279 + } else {
  280 + pickImageAndRemoveBackgroundChannel.setMessageHandler(nil)
  281 + }
268 } 282 }
269 } 283 }
  1 +import 'package:get/get.dart';
  2 +
  3 +import '../controllers/account_settings_controller.dart';
  4 +
  5 +class AccountSettingsBinding extends Bindings {
  6 + @override
  7 + void dependencies() {
  8 + Get.lazyPut<AccountSettingsController>(() => AccountSettingsController());
  9 + }
  10 +}
  1 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  2 +import 'package:doublefeel_flutter/core/services/user_state_service.dart';
  3 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  4 +import 'package:get/get.dart';
  5 +
  6 +class AccountSettingsController extends GetxController {
  7 + final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>();
  8 + final UserStateService _userStateService = Get.find<UserStateService>();
  9 +
  10 + UserPreferencesStorage get userPrefs => _userPrefs;
  11 +
  12 + void executeBackLogic() {
  13 + Get.back();
  14 + }
  15 +
  16 + Future<void> logout() async {
  17 + await _userStateService.onLogout();
  18 + Get.offAllNamed(AppRoutes.initial);
  19 + }
  20 +
  21 + void openOnboarding() {
  22 + Get.offAllNamed(AppRoutes.userOnboarding);
  23 + }
  24 +}
  1 +import 'package:doublefeel_flutter/core/theme/app_colors.dart';
  2 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  3 +import 'package:doublefeel_flutter/data/models/user/user_models.dart';
  4 +import 'package:doublefeel_flutter/r.dart';
  5 +import 'package:flutter/material.dart';
  6 +import 'package:flutter/services.dart';
  7 +import 'package:get/get.dart';
  8 +
  9 +import '../controllers/account_settings_controller.dart';
  10 +
  11 +class AccountSettingsView extends GetView<AccountSettingsController> {
  12 + const AccountSettingsView({super.key});
  13 +
  14 + static const _background = Color(0xFFF5F2FF);
  15 + static const _danger = Color(0xFFFC4447);
  16 +
  17 + @override
  18 + Widget build(BuildContext context) {
  19 + return AnnotatedRegion<SystemUiOverlayStyle>(
  20 + value: const SystemUiOverlayStyle(
  21 + statusBarColor: Colors.transparent,
  22 + statusBarIconBrightness: Brightness.dark,
  23 + statusBarBrightness: Brightness.light,
  24 + systemNavigationBarColor: _background,
  25 + systemNavigationBarIconBrightness: Brightness.dark,
  26 + ),
  27 + child: Scaffold(
  28 + backgroundColor: _background,
  29 + appBar: AppBar(
  30 + backgroundColor: Colors.transparent,
  31 + surfaceTintColor: Colors.transparent,
  32 + toolbarHeight: 44.dp,
  33 + centerTitle: true,
  34 + title: Text(
  35 + '账号设置',
  36 + style: TextStyle(
  37 + color: AppColors.textPrimary,
  38 + fontSize: 16.dp,
  39 + fontWeight: FontWeight.w500,
  40 + ),
  41 + ),
  42 + leading: IconButton(
  43 + highlightColor: Colors.transparent,
  44 + splashColor: Colors.transparent,
  45 + padding: EdgeInsets.zero,
  46 + onPressed: controller.executeBackLogic,
  47 + icon: Image.asset(
  48 + R.assetsImagesNavBackIcon,
  49 + width: 28.dp,
  50 + height: 28.dp,
  51 + ),
  52 + ),
  53 + ),
  54 + body: Obx(() {
  55 + final user = controller.userPrefs.preferences.value.meUserInfo;
  56 + return Column(
  57 + children: [
  58 + SizedBox(height: 16.dp),
  59 + _AccountCard(user: user, onLogout: controller.logout),
  60 + SizedBox(height: 17.dp),
  61 + GestureDetector(
  62 + behavior: HitTestBehavior.opaque,
  63 + onTap: controller.openOnboarding,
  64 + child: Padding(
  65 + padding: EdgeInsets.symmetric(
  66 + horizontal: 32.dp,
  67 + vertical: 8.dp,
  68 + ),
  69 + child: Text(
  70 + '注销账号',
  71 + style: TextStyle(
  72 + color: _danger,
  73 + fontSize: 12.dp,
  74 + fontWeight: FontWeight.w400,
  75 + height: 1.25,
  76 + ),
  77 + ),
  78 + ),
  79 + ),
  80 + ],
  81 + );
  82 + }),
  83 + ),
  84 + );
  85 + }
  86 +}
  87 +
  88 +class _AccountCard extends StatelessWidget {
  89 + const _AccountCard({
  90 + required this.user,
  91 + required this.onLogout,
  92 + });
  93 +
  94 + final UserInfoResponse? user;
  95 + final VoidCallback onLogout;
  96 +
  97 + @override
  98 + Widget build(BuildContext context) {
  99 + return Container(
  100 + height: 110.dp,
  101 + margin: EdgeInsets.symmetric(horizontal: 15.dp),
  102 + decoration: BoxDecoration(
  103 + color: Colors.white,
  104 + borderRadius: BorderRadius.circular(16.dp),
  105 + ),
  106 + child: Column(
  107 + children: [
  108 + SizedBox(
  109 + height: 54.dp,
  110 + child: Padding(
  111 + padding: EdgeInsets.symmetric(horizontal: 20.dp),
  112 + child: Row(
  113 + children: [
  114 + Text(
  115 + '手机号',
  116 + style: TextStyle(
  117 + color: AppColors.textPrimary,
  118 + fontSize: 14.dp,
  119 + fontWeight: FontWeight.w400,
  120 + height: 1.25,
  121 + ),
  122 + ),
  123 + const Spacer(),
  124 + Text(
  125 + _formatPhone(user?.telephone),
  126 + style: TextStyle(
  127 + color: AppColors.textSecondary,
  128 + fontSize: 14.dp,
  129 + fontWeight: FontWeight.w400,
  130 + height: 1.25,
  131 + ),
  132 + ),
  133 + ],
  134 + ),
  135 + ),
  136 + ),
  137 + Padding(
  138 + padding: EdgeInsets.symmetric(horizontal: 20.dp),
  139 + child: Divider(
  140 + height: 1.dp,
  141 + thickness: 1.dp,
  142 + color: const Color(0xFFF3F3F3),
  143 + ),
  144 + ),
  145 + Expanded(
  146 + child: GestureDetector(
  147 + behavior: HitTestBehavior.opaque,
  148 + onTap: onLogout,
  149 + child: Center(
  150 + child: Text(
  151 + '退出登录',
  152 + style: TextStyle(
  153 + color: AppColors.primary,
  154 + fontSize: 14.dp,
  155 + fontWeight: FontWeight.w500,
  156 + height: 1.25,
  157 + ),
  158 + ),
  159 + ),
  160 + ),
  161 + ),
  162 + ],
  163 + ),
  164 + );
  165 + }
  166 +
  167 + String _formatPhone(String? phone) {
  168 + final normalized = phone?.replaceAll(' ', '').trim();
  169 + if (normalized == null || normalized.isEmpty) {
  170 + return '130 1478 9632';
  171 + }
  172 + if (normalized.length == 11) {
  173 + return '${normalized.substring(0, 3)} '
  174 + '${normalized.substring(3, 7)} '
  175 + '${normalized.substring(7)}';
  176 + }
  177 + return normalized;
  178 + }
  179 +}
  1 +import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
1 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 2 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
2 -import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 3 +import 'package:doublefeel_flutter/core/theme/app_colors.dart';
  4 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  5 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  6 +import 'package:doublefeel_flutter/data/models/user/user_models.dart';
  7 +import 'package:doublefeel_flutter/r.dart';
3 import 'package:flutter/material.dart'; 8 import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 9 import 'package:get/get.dart';
5 10
6 class MyTab extends StatelessWidget { 11 class MyTab extends StatelessWidget {
7 const MyTab({super.key}); 12 const MyTab({super.key});
8 13
  14 + static const _background = Color(0xFFF5F2FF);
  15 + static const _proPurple = Color(0xFF916DF5);
  16 + static const _proGold = Color(0xFFFFDF51);
  17 +
  18 + @override
  19 + Widget build(BuildContext context) {
  20 + final userPrefs = Get.find<UserPreferencesStorage>();
  21 + return Container(
  22 + color: _background,
  23 + child: SafeArea(
  24 + bottom: false,
  25 + child: Obx(() {
  26 + final user = userPrefs.preferences.value.meUserInfo;
  27 + return ListView(
  28 + physics: const ClampingScrollPhysics(),
  29 + padding: EdgeInsets.fromLTRB(16.dp, 59.dp, 16.dp, 112.dp),
  30 + children: [
  31 + _ProfileHeader(
  32 + user: user,
  33 + onTap: () => Get.toNamed(Routes.ACCOUNT_SETTINGS),
  34 + ),
  35 + SizedBox(height: 28.dp),
  36 + const _PremiumCard(),
  37 + SizedBox(height: 12.dp),
  38 + _WatchThemeCard(
  39 + onTap: () => Get.toNamed(
  40 + Routes.WATCH_THEME,
  41 + arguments: {'isPremium': false, 'hasCustomThemes': false},
  42 + ),
  43 + ),
  44 + SizedBox(height: 12.dp),
  45 + _MenuTile(
  46 + title: '账号信息',
  47 + onTap: () => Get.toNamed(Routes.ACCOUNT_SETTINGS),
  48 + ),
  49 + SizedBox(height: 12.dp),
  50 + _MenuTile(title: '帮助', onTap: () {}),
  51 + ],
  52 + );
  53 + }),
  54 + ),
  55 + );
  56 + }
  57 +}
  58 +
  59 +class _ProfileHeader extends StatelessWidget {
  60 + const _ProfileHeader({
  61 + required this.user,
  62 + required this.onTap,
  63 + });
  64 +
  65 + final UserInfoResponse? user;
  66 + final VoidCallback onTap;
  67 +
  68 + @override
  69 + Widget build(BuildContext context) {
  70 + return GestureDetector(
  71 + behavior: HitTestBehavior.opaque,
  72 + onTap: onTap,
  73 + child: SizedBox(
  74 + height: 80.dp,
  75 + child: Row(
  76 + crossAxisAlignment: CrossAxisAlignment.center,
  77 + children: [
  78 + _Avatar(avatarUrl: user?.avatar),
  79 + SizedBox(width: 12.dp),
  80 + Expanded(
  81 + child: Column(
  82 + mainAxisAlignment: MainAxisAlignment.center,
  83 + crossAxisAlignment: CrossAxisAlignment.start,
  84 + children: [
  85 + Row(
  86 + children: [
  87 + Flexible(
  88 + child: Text(
  89 + _displayName(user),
  90 + maxLines: 1,
  91 + overflow: TextOverflow.ellipsis,
  92 + style: TextStyle(
  93 + color: AppColors.textPrimary,
  94 + fontSize: 20.dp,
  95 + fontWeight: FontWeight.w500,
  96 + height: 1.25,
  97 + ),
  98 + ),
  99 + ),
  100 + SizedBox(width: 8.dp),
  101 + Icon(
  102 + Icons.edit_outlined,
  103 + color: const Color(0xFFA084EF),
  104 + size: 16.dp,
  105 + ),
  106 + ],
  107 + ),
  108 + SizedBox(height: 6.dp),
  109 + Text(
  110 + 'ID:${user?.id ?? 738293}',
  111 + maxLines: 1,
  112 + overflow: TextOverflow.ellipsis,
  113 + style: TextStyle(
  114 + color: AppColors.textSecondary,
  115 + fontSize: 14.dp,
  116 + fontWeight: FontWeight.w400,
  117 + height: 1.25,
  118 + ),
  119 + ),
  120 + ],
  121 + ),
  122 + ),
  123 + ],
  124 + ),
  125 + ),
  126 + );
  127 + }
  128 +
  129 + String _displayName(UserInfoResponse? user) {
  130 + final name = user?.nickname?.trim();
  131 + if (name != null && name.isNotEmpty) {
  132 + return name;
  133 + }
  134 + return '不爱吃热干面';
  135 + }
  136 +}
  137 +
  138 +class _Avatar extends StatelessWidget {
  139 + const _Avatar({this.avatarUrl});
  140 +
  141 + final String? avatarUrl;
  142 +
9 @override 143 @override
10 Widget build(BuildContext context) { 144 Widget build(BuildContext context) {
11 - return Center(  
12 - child: Column(  
13 - mainAxisSize: MainAxisSize.min, 145 + final url = avatarUrl?.trim();
  146 + final imageProvider = url == null || url.isEmpty
  147 + ? AssetImage(R.assetsImagesMyAvatarDefault) as ImageProvider
  148 + : NetworkImage(url);
  149 +
  150 + return SizedBox(
  151 + width: 80.dp,
  152 + height: 80.dp,
  153 + child: Stack(
14 children: [ 154 children: [
15 - GestureDetector(  
16 - onTap: () async {  
17 - var userStateService = Get.find<UserStateService>();  
18 - await userStateService.onLogout();  
19 - Get.offAllNamed(AppRoutes.initial);  
20 - },  
21 - child: Container(  
22 - padding: const EdgeInsets.all(10),  
23 - margin: const EdgeInsets.only(top: 10),  
24 - decoration: BoxDecoration(  
25 - color: Colors.red,  
26 - borderRadius: BorderRadius.circular(10), 155 + ClipOval(
  156 + child: Image(
  157 + image: imageProvider,
  158 + width: 80.dp,
  159 + height: 80.dp,
  160 + fit: BoxFit.cover,
  161 + ),
  162 + ),
  163 + Positioned(
  164 + right: 0,
  165 + bottom: 0,
  166 + child: Container(
  167 + width: 24.dp,
  168 + height: 24.dp,
  169 + decoration: BoxDecoration(
  170 + color: const Color(0xFFE8E0FF),
  171 + shape: BoxShape.circle,
  172 + border: Border.all(color: MyTab._background, width: 2.dp),
  173 + ),
  174 + child: Icon(
  175 + Icons.photo_camera_outlined,
  176 + color: const Color(0xFFA084EF),
  177 + size: 14.dp,
  178 + ),
  179 + ),
  180 + ),
  181 + ],
  182 + ),
  183 + );
  184 + }
  185 +}
  186 +
  187 +class _PremiumCard extends StatelessWidget {
  188 + const _PremiumCard();
  189 +
  190 + @override
  191 + Widget build(BuildContext context) {
  192 + return GestureDetector(
  193 + behavior: HitTestBehavior.opaque,
  194 + onTap: () => Get.toNamed(Routes.PURCHASE),
  195 + child: Container(
  196 + height: 128.dp,
  197 + decoration: BoxDecoration(
  198 + color: MyTab._proPurple,
  199 + borderRadius: BorderRadius.circular(16.dp),
  200 + ),
  201 + clipBehavior: Clip.antiAlias,
  202 + child: Stack(
  203 + children: [
  204 + Positioned(
  205 + right: 0,
  206 + bottom: 0,
  207 + child: SizedBox(
  208 + width: 151.dp,
  209 + height: 114.dp,
  210 + child: Stack(
  211 + alignment: Alignment.bottomRight,
  212 + children: [
  213 + Positioned(
  214 + left: 0,
  215 + bottom: -14.dp,
  216 + child: Container(
  217 + width: 86.dp,
  218 + height: 110.dp,
  219 + decoration: const BoxDecoration(
  220 + color: Color(0xFFD8CFF7),
  221 + borderRadius: BorderRadius.only(
  222 + topLeft: Radius.circular(12),
  223 + topRight: Radius.circular(12),
  224 + ),
  225 + ),
  226 + ),
  227 + ),
  228 + Container(
  229 + width: 80.dp,
  230 + height: 120.dp,
  231 + decoration: BoxDecoration(
  232 + color: Colors.white,
  233 + borderRadius: BorderRadius.only(
  234 + topLeft: Radius.circular(8.dp),
  235 + topRight: Radius.circular(8.dp),
  236 + ),
  237 + ),
  238 + ),
  239 + ],
27 ), 240 ),
28 - child: const Text('Logout'),  
29 - )),  
30 - GestureDetector(  
31 - onTap: () async {  
32 - Get.offAllNamed(AppRoutes.userOnboarding);  
33 - },  
34 - child: Container(  
35 - padding: const EdgeInsets.all(10),  
36 - margin: const EdgeInsets.only(top: 10),  
37 - decoration: BoxDecoration(  
38 - color: Colors.blue,  
39 - borderRadius: BorderRadius.circular(10), 241 + ),
  242 + ),
  243 + Padding(
  244 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 20.dp),
  245 + child: Column(
  246 + crossAxisAlignment: CrossAxisAlignment.start,
  247 + children: [
  248 + ShaderMask(
  249 + blendMode: BlendMode.srcIn,
  250 + shaderCallback: (bounds) => const LinearGradient(
  251 + begin: Alignment.topCenter,
  252 + end: Alignment.bottomCenter,
  253 + colors: [
  254 + Color(0xFFFFE8AE),
  255 + Color(0xFFFFFAED),
  256 + Colors.white,
  257 + ],
  258 + ).createShader(bounds),
  259 + child: Text(
  260 + '解锁专业版',
  261 + style: TextStyle(
  262 + fontSize: 16.dp,
  263 + fontWeight: FontWeight.w600,
  264 + height: 1.25,
  265 + ),
  266 + ),
  267 + ),
  268 + SizedBox(height: 5.dp),
  269 + Text(
  270 + '开启压力预警与健康陪伴之旅',
  271 + style: TextStyle(
  272 + color: const Color(0xFFC6B3FF),
  273 + fontSize: 12.dp,
  274 + fontWeight: FontWeight.w500,
  275 + height: 1.25,
  276 + ),
  277 + ),
  278 + const Spacer(),
  279 + Container(
  280 + height: 32.dp,
  281 + width: 129.dp,
  282 + alignment: Alignment.center,
  283 + decoration: BoxDecoration(
  284 + color: MyTab._proGold,
  285 + borderRadius: BorderRadius.circular(24.dp),
  286 + ),
  287 + child: Row(
  288 + mainAxisAlignment: MainAxisAlignment.center,
  289 + children: [
  290 + Icon(
  291 + Icons.workspace_premium,
  292 + color: AppColors.textPrimary,
  293 + size: 16.dp,
  294 + ),
  295 + SizedBox(width: 4.dp),
  296 + Text(
  297 + '立即解锁',
  298 + style: TextStyle(
  299 + color: AppColors.textPrimary,
  300 + fontSize: 14.dp,
  301 + fontWeight: FontWeight.w500,
  302 + height: 1.25,
  303 + ),
  304 + ),
  305 + ],
  306 + ),
  307 + ),
  308 + ],
  309 + ),
  310 + ),
  311 + ],
  312 + ),
  313 + ),
  314 + );
  315 + }
  316 +}
  317 +
  318 +class _WatchThemeCard extends StatelessWidget {
  319 + const _WatchThemeCard({required this.onTap});
  320 +
  321 + final VoidCallback onTap;
  322 +
  323 + final double _itemSize = 64;
  324 + final double _overlap = 8;
  325 +
  326 + @override
  327 + Widget build(BuildContext context) {
  328 + return GestureDetector(
  329 + behavior: HitTestBehavior.opaque,
  330 + onTap: onTap,
  331 + child: Container(
  332 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 18.dp),
  333 + decoration: BoxDecoration(
  334 + color: Colors.white,
  335 + borderRadius: BorderRadius.circular(16.dp),
  336 + ),
  337 + child: Column(
  338 + crossAxisAlignment: CrossAxisAlignment.start,
  339 + children: [
  340 + Row(
  341 + children: [
  342 + Text(
  343 + 'Watch主题',
  344 + style: TextStyle(
  345 + color: AppColors.textPrimary,
  346 + fontSize: 16.dp,
  347 + fontWeight: FontWeight.w600,
  348 + height: 1.25,
  349 + ),
40 ), 350 ),
41 - child: const Text('Onboarding'),  
42 - )),  
43 - ], 351 + const Spacer(),
  352 + Icon(
  353 + Icons.chevron_right,
  354 + color: AppColors.textTertiary,
  355 + size: 20.dp,
  356 + ),
  357 + ],
  358 + ),
  359 + SizedBox(height: 17.dp),
  360 + SizedBox(
  361 + height: _itemSize,
  362 + child: ListView.builder(
  363 + scrollDirection: Axis.horizontal,
  364 + clipBehavior: Clip.none,
  365 + itemCount: officialThemes.length,
  366 + itemBuilder: (context, index) {
  367 + return Transform.translate(
  368 + offset: Offset(index == 0 ? 0 : -_overlap * index, 0),
  369 + child: _ThemeBubble(
  370 + size: _itemSize,
  371 + assetPath: officialThemes[index]
  372 + .infoList
  373 + .firstOrNull
  374 + ?.assetPath ??
  375 + '',
  376 + ));
  377 + },
  378 +
  379 + ),
  380 + ),
  381 + SizedBox(height: 10.dp),
  382 + Text(
  383 + '支持自定义创作主题哦~',
  384 + style: TextStyle(
  385 + color: AppColors.primary,
  386 + fontSize: 12.dp,
  387 + fontWeight: FontWeight.w500,
  388 + height: 1.25,
  389 + ),
  390 + ),
  391 + ],
  392 + ),
  393 + ),
  394 + );
  395 + }
  396 +}
  397 +
  398 +class _ThemeBubble extends StatelessWidget {
  399 + const _ThemeBubble({required this.size, required this.assetPath});
  400 +
  401 + final double size;
  402 + final String assetPath;
  403 +
  404 + @override
  405 + Widget build(BuildContext context) {
  406 + return Container(
  407 + width: size,
  408 + height: size,
  409 + decoration: BoxDecoration(
  410 + color: const Color(0xFFEDE8FF),
  411 + shape: BoxShape.circle,
  412 + border: Border.all(color: Colors.white, width: 4),
  413 + ),
  414 + alignment: Alignment.center,
  415 + child: Image.asset(
  416 + assetPath,
  417 + width: 36,
  418 + height: 36,
  419 + fit: BoxFit.fill,
  420 + ),
  421 + );
  422 + }
  423 +}
  424 +
  425 +class _MenuTile extends StatelessWidget {
  426 + const _MenuTile({
  427 + required this.title,
  428 + required this.onTap,
  429 + });
  430 +
  431 + final String title;
  432 + final VoidCallback onTap;
  433 +
  434 + @override
  435 + Widget build(BuildContext context) {
  436 + return GestureDetector(
  437 + behavior: HitTestBehavior.opaque,
  438 + onTap: onTap,
  439 + child: Container(
  440 + height: 56.dp,
  441 + padding: EdgeInsets.symmetric(horizontal: 20.dp),
  442 + decoration: BoxDecoration(
  443 + color: Colors.white,
  444 + borderRadius: BorderRadius.circular(16.dp),
  445 + ),
  446 + child: Row(
  447 + children: [
  448 + Text(
  449 + title,
  450 + style: TextStyle(
  451 + color: AppColors.textPrimary,
  452 + fontSize: 14.dp,
  453 + fontWeight: FontWeight.w400,
  454 + height: 1.25,
  455 + ),
  456 + ),
  457 + const Spacer(),
  458 + Icon(
  459 + Icons.chevron_right,
  460 + color: AppColors.textTertiary,
  461 + size: 20.dp,
  462 + ),
  463 + ],
  464 + ),
44 ), 465 ),
45 ); 466 );
46 } 467 }
  1 +import 'package:get/get.dart';
  2 +
  3 +import '../controllers/watch_theme_controller.dart';
  4 +
  5 +class WatchThemeBinding extends Bindings {
  6 + @override
  7 + void dependencies() {
  8 + if (!Get.isRegistered<WatchThemeController>()) {
  9 + Get.lazyPut<WatchThemeController>(() => WatchThemeController());
  10 + }
  11 + }
  12 +}
  1 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  2 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
  3 +import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
  4 +import 'package:doublefeel_flutter/r.dart';
  5 +import 'package:flutter/material.dart';
  6 +import 'package:get/get.dart';
  7 +
  8 +import '../models/watch_theme_models.dart';
  9 +
  10 +class WatchThemeController extends GetxController {
  11 + final selectedOfficialIndex = 0.obs;
  12 + final hasCustomThemes = false.obs;
  13 + final isPremium = false.obs;
  14 + final customThemeName = ''.obs;
  15 + final agreedToSubmission = true.obs;
  16 + final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs;
  17 + final customImagePaths = RxList<String?>.filled(4, null);
  18 + late final TextEditingController themeNameController;
  19 +
  20 + @override
  21 + void onInit() {
  22 + super.onInit();
  23 + themeNameController = TextEditingController();
  24 + themeNameController.addListener(() {
  25 + customThemeName.value = themeNameController.text.trim();
  26 + });
  27 + final args = Get.arguments;
  28 + if (args is Map) {
  29 + hasCustomThemes.value = args['hasCustomThemes'] == true;
  30 + isPremium.value = args['isPremium'] == true || hasCustomThemes.value;
  31 + }
  32 + }
  33 +
  34 + @override
  35 + void onClose() {
  36 + themeNameController.dispose();
  37 + super.onClose();
  38 + }
  39 +
  40 + bool get canSaveCustomTheme =>
  41 + customThemeName.value.isNotEmpty &&
  42 + agreedToSubmission.value &&
  43 + customImagePaths.any((path) => path != null && path.isNotEmpty);
  44 +
  45 + void executeBackLogic() {
  46 + Get.back();
  47 + }
  48 +
  49 + void selectOfficialTheme(int index) {
  50 + selectedOfficialIndex.value = index;
  51 + Get.toNamed(Routes.WATCH_THEME_PREVIEW);
  52 + }
  53 +
  54 + void createCustomTheme() {
  55 + Get.toNamed(Routes.WATCH_THEME_CREATE);
  56 + }
  57 +
  58 + void previewCustomTheme() {
  59 + Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW);
  60 + }
  61 +
  62 + Future<void> pickCustomImage(int index) async {
  63 + try {
  64 + final path = await WearEngineHostApi().pickImageAndRemoveBackground();
  65 + if (path == null || path.isEmpty) {
  66 + return;
  67 + }
  68 + customImagePaths[index] = path;
  69 + } catch (error) {
  70 + AppToast.show('图片选择失败');
  71 + }
  72 + }
  73 +
  74 + Future<void> showRenameStatusDialog(int index) async {
  75 + final result = await Get.dialog<String>(
  76 + WatchThemeRenameDialog(initialValue: customStatusNames[index]),
  77 + barrierDismissible: true,
  78 + );
  79 + if (result == null || result.trim().isEmpty) {
  80 + return;
  81 + }
  82 + customStatusNames[index] = result.trim();
  83 + }
  84 +
  85 + Future<void> handleCreateBack() async {
  86 + if (!customThemeName.value.isNotEmpty &&
  87 + !customImagePaths.any((path) => path != null && path.isNotEmpty)) {
  88 + Get.back();
  89 + return;
  90 + }
  91 + final abandon = await Get.dialog<bool>(
  92 + const WatchThemeConfirmDialog(
  93 + title: '放弃编辑',
  94 + message: '关闭此页面后,已编辑的内容不会保留,是否放弃编辑?',
  95 + primaryText: '放弃编辑',
  96 + secondaryText: '继续编辑',
  97 + ),
  98 + barrierDismissible: true,
  99 + );
  100 + if (abandon == true) {
  101 + Get.back();
  102 + }
  103 + }
  104 +
  105 + void toggleAgreement() {
  106 + agreedToSubmission.toggle();
  107 + }
  108 +
  109 + void saveCustomTheme() {
  110 + if (!canSaveCustomTheme) {
  111 + return;
  112 + }
  113 + hasCustomThemes.value = true;
  114 + isPremium.value = true;
  115 + previewCustomTheme();
  116 + }
  117 +
  118 + Future<void> confirmDeleteCustomTheme() async {
  119 + final shouldDelete = await Get.dialog<bool>(
  120 + const WatchThemeConfirmDialog(
  121 + title: '删除主题',
  122 + message: '主题删除后无法恢复,确认删除该主题吗?',
  123 + primaryText: '删除',
  124 + secondaryText: '取消',
  125 + ),
  126 + barrierDismissible: true,
  127 + );
  128 + if (shouldDelete == true) {
  129 + Get.back();
  130 + }
  131 + }
  132 +
  133 + void addWatchFace() {
  134 + // Hook to NativeHostApiStubs when the native watch-face install flow is ready.
  135 + }
  136 +}
  137 +
  138 +class WatchThemeRenameDialog extends StatefulWidget {
  139 + const WatchThemeRenameDialog({super.key, required this.initialValue});
  140 +
  141 + final String initialValue;
  142 +
  143 + @override
  144 + State<WatchThemeRenameDialog> createState() => _WatchThemeRenameDialogState();
  145 +}
  146 +
  147 +class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> {
  148 + late final TextEditingController _controller;
  149 +
  150 + @override
  151 + void initState() {
  152 + super.initState();
  153 + _controller = TextEditingController(text: widget.initialValue);
  154 + }
  155 +
  156 + @override
  157 + void dispose() {
  158 + _controller.dispose();
  159 + super.dispose();
  160 + }
  161 +
  162 + @override
  163 + Widget build(BuildContext context) {
  164 + return Dialog(
  165 + backgroundColor: Colors.transparent,
  166 + insetPadding: EdgeInsets.zero,
  167 + child: Container(
  168 + width: 300,
  169 + padding: const EdgeInsets.fromLTRB(24, 24, 24, 20),
  170 + decoration: BoxDecoration(
  171 + color: Colors.white,
  172 + borderRadius: BorderRadius.circular(24),
  173 + ),
  174 + child: Column(
  175 + mainAxisSize: MainAxisSize.min,
  176 + children: [
  177 + const Text(
  178 + '修改状态名称',
  179 + style: TextStyle(
  180 + color: Color(0xFF141414),
  181 + fontSize: 18,
  182 + fontWeight: FontWeight.w600,
  183 + ),
  184 + ),
  185 + const SizedBox(height: 16),
  186 + ValueListenableBuilder<TextEditingValue>(
  187 + valueListenable: _controller,
  188 + builder: (context, value, _) {
  189 + return Container(
  190 + height: 52,
  191 + width: 252,
  192 + padding: const EdgeInsets.symmetric(horizontal: 22),
  193 + decoration: BoxDecoration(
  194 + color: const Color(0xFFF3F3F3),
  195 + borderRadius: BorderRadius.circular(27),
  196 + ),
  197 + child: Row(
  198 + children: [
  199 + Expanded(
  200 + child: TextField(
  201 + controller: _controller,
  202 + maxLength: 4,
  203 + decoration: const InputDecoration(
  204 + counterText: '',
  205 + hintText: '请输入昵称',
  206 + hintStyle: TextStyle(color: Color(0xFFB0B0B6)),
  207 + border: InputBorder.none,
  208 + isCollapsed: true,
  209 + ),
  210 + style: const TextStyle(fontSize: 16),
  211 + ),
  212 + ),
  213 + Text(
  214 + '${value.text.characters.length}/4',
  215 + style: const TextStyle(
  216 + color: Color(0xFFD9D9D9),
  217 + fontSize: 16,
  218 + ),
  219 + ),
  220 + ],
  221 + ),
  222 + );
  223 + },
  224 + ),
  225 + const SizedBox(height: 24),
  226 + ValueListenableBuilder<TextEditingValue>(
  227 + valueListenable: _controller,
  228 + builder: (context, value, _) {
  229 + final enabled = value.text.trim().isNotEmpty;
  230 + return Opacity(
  231 + opacity: enabled ? 1 : 0.4,
  232 + child: GestureDetector(
  233 + onTap: enabled ? () => Get.back(result: value.text) : null,
  234 + child: Container(
  235 + width: 220,
  236 + height: 48,
  237 + alignment: Alignment.center,
  238 + decoration: BoxDecoration(
  239 + color: const Color(0xFF845EEE),
  240 + borderRadius: BorderRadius.circular(24),
  241 + ),
  242 + child: const Text(
  243 + '保存',
  244 + style: TextStyle(
  245 + color: Colors.white,
  246 + fontSize: 14,
  247 + fontWeight: FontWeight.w500,
  248 + ),
  249 + ),
  250 + ),
  251 + ),
  252 + );
  253 + },
  254 + ),
  255 + ],
  256 + ),
  257 + ),
  258 + );
  259 + }
  260 +}
  261 +
  262 +class WatchThemeConfirmDialog extends StatelessWidget {
  263 + const WatchThemeConfirmDialog({
  264 + super.key,
  265 + required this.title,
  266 + required this.message,
  267 + required this.primaryText,
  268 + required this.secondaryText,
  269 + });
  270 +
  271 + final String title;
  272 + final String message;
  273 + final String primaryText;
  274 + final String secondaryText;
  275 +
  276 + @override
  277 + Widget build(BuildContext context) {
  278 + return Dialog(
  279 + backgroundColor: Colors.transparent,
  280 + insetPadding: EdgeInsets.zero,
  281 + child: Container(
  282 + width: 300,
  283 + padding: const EdgeInsets.fromLTRB(24, 24, 24, 20),
  284 + decoration: BoxDecoration(
  285 + color: Colors.white,
  286 + borderRadius: BorderRadius.circular(24),
  287 + ),
  288 + child: Column(
  289 + mainAxisSize: MainAxisSize.min,
  290 + children: [
  291 + Text(
  292 + title,
  293 + style: const TextStyle(
  294 + color: Color(0xFF141414),
  295 + fontSize: 18,
  296 + fontWeight: FontWeight.w600,
  297 + ),
  298 + ),
  299 + const SizedBox(height: 16),
  300 + Text(
  301 + message,
  302 + textAlign: TextAlign.center,
  303 + style: const TextStyle(
  304 + color: Color(0xFF6E6D80),
  305 + fontSize: 15,
  306 + height: 1.35,
  307 + ),
  308 + ),
  309 + const SizedBox(height: 20),
  310 + GestureDetector(
  311 + onTap: () => Get.back(result: true),
  312 + child: Container(
  313 + width: 220,
  314 + height: 48,
  315 + alignment: Alignment.center,
  316 + decoration: BoxDecoration(
  317 + color: const Color(0xFF845EEE),
  318 + borderRadius: BorderRadius.circular(24),
  319 + ),
  320 + child: Text(
  321 + primaryText,
  322 + style: const TextStyle(
  323 + color: Colors.white,
  324 + fontSize: 14,
  325 + fontWeight: FontWeight.w500,
  326 + ),
  327 + ),
  328 + ),
  329 + ),
  330 + const SizedBox(height: 4),
  331 + GestureDetector(
  332 + onTap: () => Get.back(result: false),
  333 + child: SizedBox(
  334 + width: 220,
  335 + height: 48,
  336 + child: Center(
  337 + child: Text(
  338 + secondaryText,
  339 + style: const TextStyle(
  340 + color: Color(0xFF6E6D80),
  341 + fontSize: 14,
  342 + fontWeight: FontWeight.w500,
  343 + ),
  344 + ),
  345 + ),
  346 + ),
  347 + ),
  348 + ],
  349 + ),
  350 + ),
  351 + );
  352 + }
  353 +}
  1 +import 'package:doublefeel_flutter/r.dart';
  2 +import 'package:flutter/foundation.dart';
  3 +
  4 +class WatchThemeItem {
  5 + const WatchThemeItem({
  6 + this.id,
  7 + this.isCustomTheme = false,
  8 + this.isDefaultCharacter = false,
  9 + required this.title,
  10 + required this.infoList,
  11 + });
  12 +
  13 + final int? id;
  14 + final bool isDefaultCharacter;
  15 + final bool isCustomTheme;
  16 + final String title;
  17 + final List<WatchThemeItemInfo> infoList;
  18 +}
  19 +
  20 +class WatchThemeItemInfo {
  21 + final String title;
  22 + final String? imgUrl;
  23 + final String? assetPath;
  24 +
  25 + WatchThemeItemInfo({required this.title, this.imgUrl, this.assetPath});
  26 +}
  27 +
  28 +final officialThemes = <WatchThemeItem>[
  29 + WatchThemeItem(
  30 + title: '默认主题',
  31 + isDefaultCharacter: true,
  32 + infoList: [
  33 + WatchThemeItemInfo(
  34 + title: '状态优秀',
  35 + assetPath: 'assets/images/watch_theme/official_default_green.png',
  36 + ),
  37 + WatchThemeItemInfo(
  38 + title: '状态正常',
  39 + assetPath: 'assets/images/watch_theme/official_default_blue.png',
  40 + ),
  41 + WatchThemeItemInfo(
  42 + title: '注意压力',
  43 + assetPath: 'assets/images/watch_theme/official_default_orange.png',
  44 + ),
  45 + WatchThemeItemInfo(
  46 + title: '压力过载',
  47 + assetPath: 'assets/images/watch_theme/official_default_red.png',
  48 + ),
  49 + ],
  50 + ),
  51 + WatchThemeItem(
  52 + title: '垂耳粉兔',
  53 + infoList: [
  54 + WatchThemeItemInfo(
  55 + title: '状态优秀',
  56 + assetPath: 'assets/images/watch_theme/official_rabbit_pink.png',
  57 + ),
  58 + WatchThemeItemInfo(
  59 + title: '状态正常',
  60 + assetPath: 'assets/images/watch_theme/official_rabbit_pink.png',
  61 + ),
  62 + WatchThemeItemInfo(
  63 + title: '注意压力',
  64 + assetPath: 'assets/images/watch_theme/official_rabbit_pink.png',
  65 + ),
  66 + WatchThemeItemInfo(
  67 + title: '压力过载',
  68 + assetPath: 'assets/images/watch_theme/official_rabbit_pink.png',
  69 + ),
  70 + ],
  71 + ),
  72 + WatchThemeItem(
  73 + title: '蓝象哥哥',
  74 + infoList: [
  75 + WatchThemeItemInfo(
  76 + title: '状态优秀',
  77 + assetPath: 'assets/images/watch_theme/official_elephant_blue.png',
  78 + ),
  79 + WatchThemeItemInfo(
  80 + title: '状态正常',
  81 + assetPath: 'assets/images/watch_theme/official_elephant_blue.png',
  82 + ),
  83 + WatchThemeItemInfo(
  84 + title: '注意压力',
  85 + assetPath: 'assets/images/watch_theme/official_elephant_blue.png',
  86 + ),
  87 + WatchThemeItemInfo(
  88 + title: '压力过载',
  89 + assetPath: 'assets/images/watch_theme/official_elephant_blue.png',
  90 + ),
  91 + ],
  92 + ),
  93 + WatchThemeItem(
  94 + title: '小狗白白',
  95 + infoList: [
  96 + WatchThemeItemInfo(
  97 + title: '状态优秀',
  98 + assetPath: 'assets/images/watch_theme/official_dog_white.png',
  99 + ),
  100 + WatchThemeItemInfo(
  101 + title: '状态正常',
  102 + assetPath: 'assets/images/watch_theme/official_dog_white.png',
  103 + ),
  104 + WatchThemeItemInfo(
  105 + title: '注意压力',
  106 + assetPath: 'assets/images/watch_theme/official_dog_white.png',
  107 + ),
  108 + WatchThemeItemInfo(
  109 + title: '压力过载',
  110 + assetPath: 'assets/images/watch_theme/official_dog_white.png',
  111 + ),
  112 + ],
  113 + ),
  114 + WatchThemeItem(
  115 + title: '猫猫狗狗',
  116 + infoList: [
  117 + WatchThemeItemInfo(
  118 + title: '状态优秀',
  119 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  120 + ),
  121 + WatchThemeItemInfo(
  122 + title: '状态正常',
  123 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  124 + ),
  125 + WatchThemeItemInfo(
  126 + title: '注意压力',
  127 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  128 + ),
  129 + WatchThemeItemInfo(
  130 + title: '压力过载',
  131 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  132 + ),
  133 + ],
  134 + ),
  135 +];
  136 +
  137 +final customThemes = <WatchThemeItem>[
  138 + WatchThemeItem(
  139 + title: '猫猫狗狗-AAA',
  140 + infoList: [
  141 + WatchThemeItemInfo(
  142 + title: '状态优秀',
  143 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  144 + ),
  145 + WatchThemeItemInfo(
  146 + title: '状态正常',
  147 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  148 + ),
  149 + WatchThemeItemInfo(
  150 + title: '注意压力',
  151 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  152 + ),
  153 + WatchThemeItemInfo(
  154 + title: '压力过载',
  155 + assetPath: 'assets/images/watch_theme/custom_cat_dog.png',
  156 + ),
  157 + ],
  158 + ),
  159 +];
  1 +import 'dart:io';
  2 +
  3 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  4 +import 'package:doublefeel_flutter/r.dart';
  5 +import 'package:flutter/material.dart';
  6 +import 'package:flutter/services.dart';
  7 +import 'package:get/get.dart';
  8 +
  9 +import '../controllers/watch_theme_controller.dart';
  10 +import '../widgets/watch_face_preview.dart';
  11 +import '../widgets/watch_theme_colors.dart';
  12 +import '../widgets/watch_theme_nav_bar.dart';
  13 +
  14 +class CreateWatchThemeView extends GetView<WatchThemeController> {
  15 + const CreateWatchThemeView({super.key});
  16 +
  17 + @override
  18 + Widget build(BuildContext context) {
  19 + return AnnotatedRegion<SystemUiOverlayStyle>(
  20 + value: const SystemUiOverlayStyle(
  21 + statusBarColor: Colors.transparent,
  22 + statusBarIconBrightness: Brightness.dark,
  23 + statusBarBrightness: Brightness.light,
  24 + systemNavigationBarColor: WatchThemeColors.background,
  25 + systemNavigationBarIconBrightness: Brightness.dark,
  26 + ),
  27 + child: PopScope(
  28 + canPop: false,
  29 + onPopInvokedWithResult: (_, __) => controller.handleCreateBack(),
  30 + child: Scaffold(
  31 + backgroundColor: WatchThemeColors.background,
  32 + body: Stack(
  33 + children: [
  34 + const Positioned(
  35 + left: 0,
  36 + right: 0,
  37 + top: 0,
  38 + height: 300,
  39 + child: DecoratedBox(
  40 + decoration: BoxDecoration(
  41 + gradient: LinearGradient(
  42 + begin: Alignment.topCenter,
  43 + end: Alignment.bottomCenter,
  44 + colors: [
  45 + WatchThemeColors.gradientTop,
  46 + WatchThemeColors.background,
  47 + ],
  48 + ),
  49 + ),
  50 + ),
  51 + ),
  52 + SafeArea(
  53 + bottom: false,
  54 + child: Column(
  55 + children: [
  56 + WatchThemeNavBar(
  57 + title: '创作主题',
  58 + onBack: controller.handleCreateBack,
  59 + ),
  60 + Expanded(
  61 + child: SingleChildScrollView(
  62 + physics: const ClampingScrollPhysics(),
  63 + padding: EdgeInsets.only(bottom: 28.dp),
  64 + child: Column(
  65 + children: [
  66 + SizedBox(height: 12.dp),
  67 + WatchFacePreview(
  68 + faceAsset:
  69 + R.assetsImagesWatchThemeCustomFaceCreate,
  70 + ),
  71 + SizedBox(height: 20.dp),
  72 + _EditorCard(controller: controller),
  73 + SizedBox(height: 26.dp),
  74 + _AgreementRow(controller: controller),
  75 + SizedBox(height: 18.dp),
  76 + _SaveButton(controller: controller),
  77 + ],
  78 + ),
  79 + ),
  80 + ),
  81 + ],
  82 + ),
  83 + ),
  84 + ],
  85 + ),
  86 + ),
  87 + ),
  88 + );
  89 + }
  90 +}
  91 +
  92 +class _EditorCard extends StatelessWidget {
  93 + const _EditorCard({required this.controller});
  94 +
  95 + final WatchThemeController controller;
  96 +
  97 + static const _statusColors = [
  98 + WatchThemeColors.excellent,
  99 + WatchThemeColors.normal,
  100 + WatchThemeColors.stress,
  101 + WatchThemeColors.overload,
  102 + ];
  103 +
  104 + @override
  105 + Widget build(BuildContext context) {
  106 + return Container(
  107 + margin: EdgeInsets.symmetric(horizontal: 16.dp),
  108 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 20.dp),
  109 + decoration: BoxDecoration(
  110 + color: Colors.white,
  111 + borderRadius: BorderRadius.circular(16.dp),
  112 + ),
  113 + child: Column(
  114 + crossAxisAlignment: CrossAxisAlignment.start,
  115 + children: [
  116 + Text(
  117 + '自定义主题',
  118 + style: TextStyle(
  119 + color: WatchThemeColors.textPrimary,
  120 + fontSize: 16.dp,
  121 + fontWeight: FontWeight.w500,
  122 + ),
  123 + ),
  124 + SizedBox(height: 4.dp),
  125 + Text(
  126 + '支持上传图片、视频、Live图、Gif、最长5秒',
  127 + style: TextStyle(
  128 + color: WatchThemeColors.textSecondary,
  129 + fontSize: 12.dp,
  130 + ),
  131 + ),
  132 + SizedBox(height: 14.dp),
  133 + Obx(
  134 + () => GridView.builder(
  135 + shrinkWrap: true,
  136 + padding: EdgeInsets.zero,
  137 + physics: const NeverScrollableScrollPhysics(),
  138 + itemCount: 4,
  139 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  140 + crossAxisCount: 2,
  141 + crossAxisSpacing: 23.dp,
  142 + mainAxisSpacing: 14.dp,
  143 + mainAxisExtent: 170.dp,
  144 + ),
  145 + itemBuilder: (context, index) {
  146 + return _UploadTile(
  147 + index: index,
  148 + color: _statusColors[index],
  149 + label: controller.customStatusNames[index],
  150 + path: controller.customImagePaths[index],
  151 + onPick: () => controller.pickCustomImage(index),
  152 + onRename: () => controller.showRenameStatusDialog(index),
  153 + );
  154 + },
  155 + ),
  156 + ),
  157 + SizedBox(height: 14.dp),
  158 + Text(
  159 + '主题名称',
  160 + style: TextStyle(
  161 + color: WatchThemeColors.textPrimary,
  162 + fontSize: 16.dp,
  163 + fontWeight: FontWeight.w500,
  164 + ),
  165 + ),
  166 + SizedBox(height: 14.dp),
  167 + Container(
  168 + height: 52.dp,
  169 + padding: EdgeInsets.symmetric(horizontal: 26.dp),
  170 + decoration: BoxDecoration(
  171 + color: WatchThemeColors.background,
  172 + borderRadius: BorderRadius.circular(16.dp),
  173 + ),
  174 + alignment: Alignment.center,
  175 + child: TextField(
  176 + controller: controller.themeNameController,
  177 + maxLength: 10,
  178 + decoration: InputDecoration(
  179 + counterText: '',
  180 + hintText: '最多10个字符',
  181 + hintStyle: TextStyle(
  182 + color: WatchThemeColors.textTertiary,
  183 + fontSize: 14.dp,
  184 + ),
  185 + border: InputBorder.none,
  186 + isCollapsed: true,
  187 + ),
  188 + style: TextStyle(
  189 + color: WatchThemeColors.textPrimary,
  190 + fontSize: 14.dp,
  191 + ),
  192 + ),
  193 + ),
  194 + ],
  195 + ),
  196 + );
  197 + }
  198 +}
  199 +
  200 +class _UploadTile extends StatelessWidget {
  201 + const _UploadTile({
  202 + required this.index,
  203 + required this.color,
  204 + required this.label,
  205 + required this.path,
  206 + required this.onPick,
  207 + required this.onRename,
  208 + });
  209 +
  210 + final int index;
  211 + final Color color;
  212 + final String label;
  213 + final String? path;
  214 + final VoidCallback onPick;
  215 + final VoidCallback onRename;
  216 +
  217 + @override
  218 + Widget build(BuildContext context) {
  219 + return Column(
  220 + children: [
  221 + GestureDetector(
  222 + behavior: HitTestBehavior.opaque,
  223 + onTap: onPick,
  224 + child: Container(
  225 + width: 140.dp,
  226 + height: 140.dp,
  227 + decoration: BoxDecoration(
  228 + color: WatchThemeColors.background,
  229 + borderRadius: BorderRadius.circular(18.667.dp),
  230 + ),
  231 + clipBehavior: Clip.antiAlias,
  232 + child: path == null
  233 + ? Icon(Icons.add, size: 34.dp, color: color)
  234 + : Image.file(File(path!), fit: BoxFit.cover),
  235 + ),
  236 + ),
  237 + SizedBox(height: 10.dp),
  238 + Row(
  239 + mainAxisAlignment: MainAxisAlignment.center,
  240 + children: [
  241 + Text(
  242 + label,
  243 + style: TextStyle(
  244 + color: WatchThemeColors.textPrimary,
  245 + fontSize: 12.dp,
  246 + ),
  247 + ),
  248 + SizedBox(width: 4.dp),
  249 + GestureDetector(
  250 + onTap: onRename,
  251 + child: Icon(
  252 + Icons.edit_outlined,
  253 + color: const Color(0xFFA084EF),
  254 + size: 14.dp,
  255 + ),
  256 + ),
  257 + ],
  258 + ),
  259 + ],
  260 + );
  261 + }
  262 +}
  263 +
  264 +class _AgreementRow extends StatelessWidget {
  265 + const _AgreementRow({required this.controller});
  266 +
  267 + final WatchThemeController controller;
  268 +
  269 + @override
  270 + Widget build(BuildContext context) {
  271 + return Obx(
  272 + () => GestureDetector(
  273 + behavior: HitTestBehavior.opaque,
  274 + onTap: controller.toggleAgreement,
  275 + child: Row(
  276 + mainAxisAlignment: MainAxisAlignment.center,
  277 + children: [
  278 + Container(
  279 + width: 16.dp,
  280 + height: 16.dp,
  281 + decoration: BoxDecoration(
  282 + color: controller.agreedToSubmission.value
  283 + ? WatchThemeColors.brand
  284 + : Colors.transparent,
  285 + border: Border.all(color: WatchThemeColors.brand, width: 1.dp),
  286 + shape: BoxShape.circle,
  287 + ),
  288 + child: controller.agreedToSubmission.value
  289 + ? Icon(Icons.check, color: Colors.white, size: 12.dp)
  290 + : null,
  291 + ),
  292 + SizedBox(width: 4.dp),
  293 + Text(
  294 + '我已阅读并同意用户投稿协议',
  295 + style: TextStyle(
  296 + color: WatchThemeColors.textSecondary,
  297 + fontSize: 12.dp,
  298 + ),
  299 + ),
  300 + ],
  301 + ),
  302 + ),
  303 + );
  304 + }
  305 +}
  306 +
  307 +class _SaveButton extends StatelessWidget {
  308 + const _SaveButton({required this.controller});
  309 +
  310 + final WatchThemeController controller;
  311 +
  312 + @override
  313 + Widget build(BuildContext context) {
  314 + return Obx(
  315 + () {
  316 + final enabled = controller.canSaveCustomTheme;
  317 + return Opacity(
  318 + opacity: enabled ? 1 : 0.4,
  319 + child: GestureDetector(
  320 + onTap: enabled ? controller.saveCustomTheme : null,
  321 + child: Container(
  322 + width: 280.dp,
  323 + height: 48.dp,
  324 + alignment: Alignment.center,
  325 + decoration: BoxDecoration(
  326 + color: WatchThemeColors.brand,
  327 + borderRadius: BorderRadius.circular(24.dp),
  328 + ),
  329 + child: Text(
  330 + '保存主题',
  331 + style: TextStyle(
  332 + color: Colors.white,
  333 + fontSize: 16.dp,
  334 + fontWeight: FontWeight.w600,
  335 + ),
  336 + ),
  337 + ),
  338 + ),
  339 + );
  340 + },
  341 + );
  342 + }
  343 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:doublefeel_flutter/r.dart';
  3 +import 'package:flutter/material.dart';
  4 +import 'package:flutter/services.dart';
  5 +import 'package:get/get.dart';
  6 +
  7 +import '../controllers/watch_theme_controller.dart';
  8 +import '../widgets/watch_face_preview.dart';
  9 +import '../widgets/watch_theme_bottom_actions.dart';
  10 +import '../widgets/watch_theme_colors.dart';
  11 +import '../widgets/watch_theme_header.dart';
  12 +import '../widgets/watch_theme_nav_bar.dart';
  13 +import '../widgets/watch_theme_section_card.dart';
  14 +
  15 +class CustomWatchThemePreviewView extends GetView<WatchThemeController> {
  16 + const CustomWatchThemePreviewView({super.key});
  17 +
  18 + @override
  19 + Widget build(BuildContext context) {
  20 + return AnnotatedRegion<SystemUiOverlayStyle>(
  21 + value: const SystemUiOverlayStyle(
  22 + statusBarColor: Colors.transparent,
  23 + statusBarIconBrightness: Brightness.dark,
  24 + statusBarBrightness: Brightness.light,
  25 + systemNavigationBarColor: WatchThemeColors.background,
  26 + systemNavigationBarIconBrightness: Brightness.dark,
  27 + ),
  28 + child: Scaffold(
  29 + backgroundColor: WatchThemeColors.background,
  30 + body: Stack(
  31 + children: [
  32 + const Positioned(
  33 + left: 0,
  34 + right: 0,
  35 + top: 0,
  36 + height: 300,
  37 + child: DecoratedBox(
  38 + decoration: BoxDecoration(
  39 + gradient: LinearGradient(
  40 + begin: Alignment.topCenter,
  41 + end: Alignment.bottomCenter,
  42 + colors: [
  43 + WatchThemeColors.gradientTop,
  44 + WatchThemeColors.background,
  45 + ],
  46 + ),
  47 + ),
  48 + ),
  49 + ),
  50 + SafeArea(
  51 + bottom: false,
  52 + child: Column(
  53 + children: [
  54 + WatchThemeNavBar(
  55 + title: '预览',
  56 + onBack: controller.executeBackLogic,
  57 + trailing: GestureDetector(
  58 + onTap: controller.confirmDeleteCustomTheme,
  59 + child: Text(
  60 + '删除',
  61 + style: TextStyle(
  62 + color: const Color(0xFFFC4447),
  63 + fontSize: 14.dp,
  64 + fontWeight: FontWeight.w500,
  65 + ),
  66 + ),
  67 + ),
  68 + ),
  69 + Expanded(
  70 + child: SingleChildScrollView(
  71 + physics: const ClampingScrollPhysics(),
  72 + padding: EdgeInsets.only(bottom: 96.dp),
  73 + child: Obx(
  74 + () => Column(
  75 + children: [
  76 + WatchThemeHeader(
  77 + title: controller.customThemeName.value.isEmpty
  78 + ? '默认主题'
  79 + : controller.customThemeName.value,
  80 + faceAsset:
  81 + R.assetsImagesWatchThemeCustomFacePreview,
  82 + ),
  83 + SizedBox(height: 26.dp),
  84 + _CustomStatusPreviewCard(controller: controller),
  85 + SizedBox(height: 12.dp),
  86 + const _CustomDialPreviewCard(),
  87 + ],
  88 + ),
  89 + ),
  90 + ),
  91 + ),
  92 + ],
  93 + ),
  94 + ),
  95 + ],
  96 + ),
  97 + bottomNavigationBar: WatchThemeBottomActions(
  98 + onAddTap: controller.addWatchFace,
  99 + secondaryLabel: '立即使用',
  100 + onSecondaryTap: controller.addWatchFace,
  101 + secondaryDisabled: false,
  102 + ),
  103 + ),
  104 + );
  105 + }
  106 +}
  107 +
  108 +class _CustomStatusPreviewCard extends StatelessWidget {
  109 + const _CustomStatusPreviewCard({required this.controller});
  110 +
  111 + final WatchThemeController controller;
  112 +
  113 + static final _assets = [
  114 + R.assetsImagesWatchThemeCustomStatusExcellent,
  115 + R.assetsImagesWatchThemeCustomStatusNormal,
  116 + R.assetsImagesWatchThemeCustomStatusStress,
  117 + R.assetsImagesWatchThemeCustomStatusOverload,
  118 + ];
  119 +
  120 + static const _colors = [
  121 + WatchThemeColors.excellent,
  122 + WatchThemeColors.normal,
  123 + WatchThemeColors.stress,
  124 + WatchThemeColors.overload,
  125 + ];
  126 +
  127 + @override
  128 + Widget build(BuildContext context) {
  129 + return WatchThemeSectionCard(
  130 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp),
  131 + child: Column(
  132 + crossAxisAlignment: CrossAxisAlignment.start,
  133 + children: [
  134 + const WatchThemeSectionTitle('状态预览'),
  135 + SizedBox(height: 24.dp),
  136 + Obx(
  137 + () => Row(
  138 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  139 + children: [
  140 + for (var i = 0; i < 4; i++)
  141 + Column(
  142 + children: [
  143 + ClipRRect(
  144 + borderRadius: BorderRadius.circular(16.dp),
  145 + child: Image.asset(
  146 + _assets[i],
  147 + width: 60.dp,
  148 + height: 60.dp,
  149 + fit: BoxFit.cover,
  150 + ),
  151 + ),
  152 + SizedBox(height: 4.dp),
  153 + Text(
  154 + controller.customStatusNames[i],
  155 + style: TextStyle(
  156 + color: _colors[i],
  157 + fontSize: 12.dp,
  158 + ),
  159 + ),
  160 + ],
  161 + ),
  162 + ],
  163 + ),
  164 + ),
  165 + ],
  166 + ),
  167 + );
  168 + }
  169 +}
  170 +
  171 +class _CustomDialPreviewCard extends StatelessWidget {
  172 + const _CustomDialPreviewCard();
  173 +
  174 + @override
  175 + Widget build(BuildContext context) {
  176 + return WatchThemeSectionCard(
  177 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp),
  178 + child: Column(
  179 + crossAxisAlignment: CrossAxisAlignment.start,
  180 + children: [
  181 + const WatchThemeSectionTitle('表盘预览'),
  182 + SizedBox(height: 16.dp),
  183 + Row(
  184 + children: [
  185 + WatchFacePreview(
  186 + width: 114,
  187 + height: 136,
  188 + faceAsset: R.assetsImagesWatchThemeCustomPreviewAlt,
  189 + ),
  190 + SizedBox(width: 18.dp),
  191 + WatchFacePreview(
  192 + width: 114,
  193 + height: 136,
  194 + faceAsset: R.assetsImagesWatchThemeCustomFacePreview,
  195 + ),
  196 + ],
  197 + ),
  198 + ],
  199 + ),
  200 + );
  201 + }
  202 +}
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter/services.dart';
  3 +import 'package:get/get.dart';
  4 +
  5 +import '../controllers/watch_theme_controller.dart';
  6 +import '../widgets/dial_preview_card.dart';
  7 +import '../widgets/status_preview_card.dart';
  8 +import '../widgets/watch_theme_bottom_actions.dart';
  9 +import '../widgets/watch_theme_colors.dart';
  10 +import '../widgets/watch_theme_header.dart';
  11 +import '../widgets/watch_theme_nav_bar.dart';
  12 +
  13 +class WatchThemePreviewView extends GetView<WatchThemeController> {
  14 + const WatchThemePreviewView({super.key});
  15 +
  16 + @override
  17 + Widget build(BuildContext context) {
  18 + return AnnotatedRegion<SystemUiOverlayStyle>(
  19 + value: const SystemUiOverlayStyle(
  20 + statusBarColor: Colors.transparent,
  21 + statusBarIconBrightness: Brightness.dark,
  22 + statusBarBrightness: Brightness.light,
  23 + systemNavigationBarColor: WatchThemeColors.background,
  24 + systemNavigationBarIconBrightness: Brightness.dark,
  25 + ),
  26 + child: Scaffold(
  27 + backgroundColor: WatchThemeColors.background,
  28 + body: Stack(
  29 + children: [
  30 + const Positioned(
  31 + left: 0,
  32 + right: 0,
  33 + top: 0,
  34 + height: 300,
  35 + child: DecoratedBox(
  36 + decoration: BoxDecoration(
  37 + gradient: LinearGradient(
  38 + begin: Alignment.topCenter,
  39 + end: Alignment.bottomCenter,
  40 + colors: [
  41 + WatchThemeColors.gradientTop,
  42 + WatchThemeColors.background,
  43 + ],
  44 + ),
  45 + ),
  46 + ),
  47 + ),
  48 + SafeArea(
  49 + bottom: false,
  50 + child: Column(
  51 + children: [
  52 + WatchThemeNavBar(
  53 + title: '预览',
  54 + onBack: controller.executeBackLogic,
  55 + ),
  56 + Expanded(
  57 + child: SingleChildScrollView(
  58 + physics: const ClampingScrollPhysics(),
  59 + padding: const EdgeInsets.only(bottom: 96),
  60 + child: Column(
  61 + children: const [
  62 + WatchThemeHeader(),
  63 + SizedBox(height: 26),
  64 + StatusPreviewCard(),
  65 + SizedBox(height: 12),
  66 + DialPreviewCard(),
  67 + ],
  68 + ),
  69 + ),
  70 + ),
  71 + ],
  72 + ),
  73 + ),
  74 + ],
  75 + ),
  76 + bottomNavigationBar: WatchThemeBottomActions(
  77 + onAddTap: controller.addWatchFace,
  78 + ),
  79 + ),
  80 + );
  81 + }
  82 +}
  1 +import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:flutter/services.dart';
  4 +import 'package:get/get.dart';
  5 +
  6 +import '../controllers/watch_theme_controller.dart';
  7 +import '../widgets/custom_theme_card.dart';
  8 +import '../widgets/official_theme_grid.dart';
  9 +import '../widgets/watch_theme_colors.dart';
  10 +import '../widgets/watch_theme_header.dart';
  11 +import '../widgets/watch_theme_nav_bar.dart';
  12 +
  13 +class WatchThemeView extends GetView<WatchThemeController> {
  14 + const WatchThemeView({super.key});
  15 +
  16 + @override
  17 + Widget build(BuildContext context) {
  18 + return AnnotatedRegion<SystemUiOverlayStyle>(
  19 + value: const SystemUiOverlayStyle(
  20 + statusBarColor: Colors.transparent,
  21 + statusBarIconBrightness: Brightness.dark,
  22 + statusBarBrightness: Brightness.light,
  23 + systemNavigationBarColor: WatchThemeColors.background,
  24 + systemNavigationBarIconBrightness: Brightness.dark,
  25 + ),
  26 + child: Scaffold(
  27 + backgroundColor: WatchThemeColors.background,
  28 + body: Stack(
  29 + children: [
  30 + const Positioned(
  31 + left: 0,
  32 + right: 0,
  33 + top: 0,
  34 + height: 300,
  35 + child: DecoratedBox(
  36 + decoration: BoxDecoration(
  37 + gradient: LinearGradient(
  38 + begin: Alignment.topCenter,
  39 + end: Alignment.bottomCenter,
  40 + colors: [
  41 + WatchThemeColors.gradientTop,
  42 + WatchThemeColors.background,
  43 + ],
  44 + ),
  45 + ),
  46 + ),
  47 + ),
  48 + SafeArea(
  49 + bottom: false,
  50 + child: Column(
  51 + children: [
  52 + WatchThemeNavBar(
  53 + title: 'Watch主题',
  54 + onBack: controller.executeBackLogic,
  55 + ),
  56 + Expanded(
  57 + child: Obx(
  58 + () => SingleChildScrollView(
  59 + physics: const BouncingScrollPhysics(),
  60 + padding: const EdgeInsets.only(bottom: 24),
  61 + child: Column(
  62 + children: [
  63 + const WatchThemeHeader(),
  64 + const SizedBox(height: 26),
  65 + OfficialThemeGrid(
  66 + themes: officialThemes,
  67 + selectedIndex:
  68 + controller.selectedOfficialIndex.value,
  69 + onThemeTap: controller.selectOfficialTheme,
  70 + ),
  71 + const SizedBox(height: 12),
  72 + CustomThemeCard(
  73 + isPremium: controller.isPremium.value,
  74 + hasCustomThemes: controller.hasCustomThemes.value,
  75 + customThemes: customThemes,
  76 + onCreateTap: controller.createCustomTheme,
  77 + onThemeTap: (_) =>
  78 + controller.previewCustomTheme(),
  79 + ),
  80 + ],
  81 + ),
  82 + ),
  83 + ),
  84 + ),
  85 + ],
  86 + ),
  87 + ),
  88 + ],
  89 + ),
  90 + ),
  91 + );
  92 + }
  93 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import '../models/watch_theme_models.dart';
  5 +import 'watch_theme_character_avatar.dart';
  6 +import 'watch_theme_colors.dart';
  7 +import 'watch_theme_pro_badge.dart';
  8 +import 'watch_theme_section_card.dart';
  9 +
  10 +class CustomThemeCard extends StatelessWidget {
  11 + const CustomThemeCard({
  12 + super.key,
  13 + required this.isPremium,
  14 + required this.hasCustomThemes,
  15 + required this.customThemes,
  16 + required this.onCreateTap,
  17 + this.onThemeTap,
  18 + });
  19 +
  20 + final bool isPremium;
  21 + final bool hasCustomThemes;
  22 + final List<WatchThemeItem> customThemes;
  23 + final VoidCallback onCreateTap;
  24 + final ValueChanged<WatchThemeItem>? onThemeTap;
  25 +
  26 + @override
  27 + Widget build(BuildContext context) {
  28 + return WatchThemeSectionCard(
  29 + child: Column(
  30 + crossAxisAlignment: CrossAxisAlignment.start,
  31 + children: [
  32 + WatchThemeSectionTitle(
  33 + '自定义主题',
  34 + trailing: isPremium ? null : const WatchThemeProBadge(),
  35 + ),
  36 + SizedBox(height: 4.dp),
  37 + Text(
  38 + '用创意记录每一次情绪波动,打造懂你的专属表盘吧~',
  39 + maxLines: 1,
  40 + overflow: TextOverflow.ellipsis,
  41 + style: TextStyle(
  42 + color: WatchThemeColors.textSecondary,
  43 + fontSize: 12.dp,
  44 + fontWeight: FontWeight.w400,
  45 + height: 1.25,
  46 + ),
  47 + ),
  48 + SizedBox(height: 14.dp),
  49 + _CreateThemeButton(onTap: onCreateTap),
  50 + if (hasCustomThemes) ...[
  51 + SizedBox(height: 12.dp),
  52 + Row(
  53 + children: [
  54 + for (var index = 0; index < customThemes.length; index++) ...[
  55 + _CustomThemeItem(
  56 + item: customThemes[index],
  57 + onTap: onThemeTap == null
  58 + ? null
  59 + : () => onThemeTap!(customThemes[index]),
  60 + ),
  61 + if (index != customThemes.length - 1) SizedBox(width: 20.dp),
  62 + ],
  63 + ],
  64 + ),
  65 + ],
  66 + ],
  67 + ),
  68 + );
  69 + }
  70 +}
  71 +
  72 +class _CreateThemeButton extends StatelessWidget {
  73 + const _CreateThemeButton({required this.onTap});
  74 +
  75 + final VoidCallback onTap;
  76 +
  77 + @override
  78 + Widget build(BuildContext context) {
  79 + return GestureDetector(
  80 + behavior: HitTestBehavior.opaque,
  81 + onTap: onTap,
  82 + child: Container(
  83 + height: 48.dp,
  84 + padding: EdgeInsets.symmetric(horizontal: 20.dp),
  85 + decoration: BoxDecoration(
  86 + borderRadius: BorderRadius.circular(12.dp),
  87 + gradient: const LinearGradient(
  88 + begin: Alignment.centerLeft,
  89 + end: Alignment.centerRight,
  90 + colors: [
  91 + Color(0xFFFFFFD0),
  92 + Color(0xFFFFD1DB),
  93 + Color(0xFFF0CDFF),
  94 + Color(0xFFCFE8FF),
  95 + ],
  96 + stops: [0, 0.4, 0.7, 1],
  97 + ),
  98 + ),
  99 + child: Row(
  100 + children: [
  101 + Icon(
  102 + Icons.palette_outlined,
  103 + color: WatchThemeColors.overload,
  104 + size: 28.dp,
  105 + ),
  106 + SizedBox(width: 8.dp),
  107 + Text(
  108 + '创作主题',
  109 + style: TextStyle(
  110 + color: WatchThemeColors.textPrimary,
  111 + fontSize: 14.dp,
  112 + fontWeight: FontWeight.w500,
  113 + height: 1.25,
  114 + ),
  115 + ),
  116 + const Spacer(),
  117 + Icon(
  118 + Icons.chevron_right,
  119 + color: WatchThemeColors.textSecondary,
  120 + size: 20.dp,
  121 + ),
  122 + ],
  123 + ),
  124 + ),
  125 + );
  126 + }
  127 +}
  128 +
  129 +class _CustomThemeItem extends StatelessWidget {
  130 + const _CustomThemeItem({required this.item, this.onTap});
  131 +
  132 + final WatchThemeItem item;
  133 + final VoidCallback? onTap;
  134 +
  135 + @override
  136 + Widget build(BuildContext context) {
  137 + return GestureDetector(
  138 + behavior: HitTestBehavior.opaque,
  139 + onTap: onTap,
  140 + child: SizedBox(
  141 + width: 88.dp,
  142 + child: Column(
  143 + children: [
  144 + WatchThemeCharacterAvatar(
  145 + size: 88,
  146 + assetPath: item.infoList.firstOrNull?.assetPath,
  147 + empty: item.infoList.firstOrNull?.assetPath == null,
  148 + ),
  149 + SizedBox(height: 8.dp),
  150 + Text(
  151 + item.title,
  152 + maxLines: 1,
  153 + overflow: TextOverflow.ellipsis,
  154 + style: TextStyle(
  155 + color: WatchThemeColors.textPrimary,
  156 + fontSize: 12.dp,
  157 + fontWeight: FontWeight.w400,
  158 + height: 1.25,
  159 + ),
  160 + ),
  161 + ],
  162 + ),
  163 + ),
  164 + );
  165 + }
  166 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:doublefeel_flutter/r.dart';
  3 +import 'package:flutter/material.dart';
  4 +
  5 +import 'watch_face_preview.dart';
  6 +import 'watch_theme_section_card.dart';
  7 +
  8 +class DialPreviewCard extends StatelessWidget {
  9 + const DialPreviewCard({super.key});
  10 +
  11 + @override
  12 + Widget build(BuildContext context) {
  13 + return WatchThemeSectionCard(
  14 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp),
  15 + child: Column(
  16 + crossAxisAlignment: CrossAxisAlignment.start,
  17 + children: [
  18 + const WatchThemeSectionTitle('表盘预览'),
  19 + SizedBox(height: 16.dp),
  20 + SizedBox(
  21 + height: 136.dp,
  22 + child: ListView.separated(
  23 + padding: EdgeInsets.zero,
  24 + scrollDirection: Axis.horizontal,
  25 + physics: const ClampingScrollPhysics(),
  26 + itemBuilder: (context, index) {
  27 + return WatchFacePreview(
  28 + width: 114,
  29 + height: 136,
  30 + faceAsset: index == 0
  31 + ? R.assetsImagesWatchThemeFacePreviewAlt
  32 + : R.assetsImagesWatchThemeFaceDefault,
  33 + );
  34 + },
  35 + separatorBuilder: (context, index) => SizedBox(width: 18.dp),
  36 + itemCount: 2,
  37 + ),
  38 + ),
  39 + ],
  40 + ),
  41 + );
  42 + }
  43 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import '../models/watch_theme_models.dart';
  5 +import 'watch_theme_character_avatar.dart';
  6 +import 'watch_theme_colors.dart';
  7 +import 'watch_theme_section_card.dart';
  8 +
  9 +class OfficialThemeGrid extends StatelessWidget {
  10 + const OfficialThemeGrid({
  11 + super.key,
  12 + required this.themes,
  13 + required this.selectedIndex,
  14 + required this.onThemeTap,
  15 + });
  16 +
  17 + final List<WatchThemeItem> themes;
  18 + final int selectedIndex;
  19 + final ValueChanged<int> onThemeTap;
  20 +
  21 + @override
  22 + Widget build(BuildContext context) {
  23 + return WatchThemeSectionCard(
  24 + child: Column(
  25 + crossAxisAlignment: CrossAxisAlignment.start,
  26 + children: [
  27 + const WatchThemeSectionTitle('官方主题'),
  28 + SizedBox(height: 22.dp),
  29 + GridView.builder(
  30 + shrinkWrap: true,
  31 + padding: EdgeInsets.zero,
  32 + physics: const NeverScrollableScrollPhysics(),
  33 + itemCount: themes.length,
  34 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  35 + crossAxisCount: 3,
  36 + mainAxisSpacing: 21.dp,
  37 + crossAxisSpacing: 19.dp,
  38 + mainAxisExtent: 106.dp,
  39 + ),
  40 + itemBuilder: (context, index) {
  41 + final theme = themes[index];
  42 + return GestureDetector(
  43 + behavior: HitTestBehavior.opaque,
  44 + onTap: () => onThemeTap(index),
  45 + child: Column(
  46 + children: [
  47 + WatchThemeCharacterAvatar(
  48 + size: 88,
  49 + selected: index == selectedIndex,
  50 + useDefaultCharacter: theme.isDefaultCharacter,
  51 + assetPath: theme.infoList.firstOrNull?.assetPath,
  52 + ),
  53 + SizedBox(height: 8.dp),
  54 + Text(
  55 + theme.title,
  56 + textAlign: TextAlign.center,
  57 + maxLines: 1,
  58 + overflow: TextOverflow.ellipsis,
  59 + style: TextStyle(
  60 + color: WatchThemeColors.textPrimary,
  61 + fontSize: 12.dp,
  62 + fontWeight: FontWeight.w400,
  63 + height: 1.25,
  64 + ),
  65 + ),
  66 + ],
  67 + ),
  68 + );
  69 + },
  70 + ),
  71 + ],
  72 + ),
  73 + );
  74 + }
  75 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_theme_character_avatar.dart';
  5 +import 'watch_theme_colors.dart';
  6 +import 'watch_theme_section_card.dart';
  7 +
  8 +class StatusPreviewCard extends StatelessWidget {
  9 + const StatusPreviewCard({super.key});
  10 +
  11 + static const _items = [
  12 + _StatusPreviewItem('状态优秀', WatchThemeColors.excellent,
  13 + icon: 'assets/images/watch_theme/official_default_green.png'),
  14 + _StatusPreviewItem('状态正常', WatchThemeColors.normal,
  15 + icon: 'assets/images/watch_theme/official_default_blue.png'),
  16 + _StatusPreviewItem('注意压力', WatchThemeColors.stress,
  17 + icon: 'assets/images/watch_theme/official_default_orange.png'),
  18 + _StatusPreviewItem('压力过载', WatchThemeColors.overload,
  19 + icon: 'assets/images/watch_theme/official_default_red.png'),
  20 + ];
  21 +
  22 + @override
  23 + Widget build(BuildContext context) {
  24 + return WatchThemeSectionCard(
  25 + padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp),
  26 + child: Column(
  27 + crossAxisAlignment: CrossAxisAlignment.start,
  28 + children: [
  29 + const WatchThemeSectionTitle('状态预览'),
  30 + SizedBox(height: 24.dp),
  31 + Row(
  32 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  33 + children: [
  34 + for (final item in _items)
  35 + Column(
  36 + children: [
  37 + WatchThemeCharacterAvatar(
  38 + size: 60,
  39 + useDefaultCharacter: true,
  40 + backgroundColor: item.color,
  41 + assetPath: item.icon,
  42 + ),
  43 + SizedBox(height: 4.dp),
  44 + Text(
  45 + item.title,
  46 + style: TextStyle(
  47 + color: item.color,
  48 + fontSize: 12.dp,
  49 + fontWeight: FontWeight.w400,
  50 + height: 1.25,
  51 + ),
  52 + ),
  53 + ],
  54 + ),
  55 + ],
  56 + ),
  57 + ],
  58 + ),
  59 + );
  60 + }
  61 +}
  62 +
  63 +class _StatusPreviewItem {
  64 + const _StatusPreviewItem(this.title, this.color, {required this.icon});
  65 +
  66 + final String title;
  67 + final Color color;
  68 + final String icon;
  69 +}
  1 +import 'dart:io';
  2 +
  3 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  4 +import 'package:doublefeel_flutter/r.dart';
  5 +import 'package:flutter/material.dart';
  6 +
  7 +import 'watch_theme_colors.dart';
  8 +
  9 +class WatchFacePreview extends StatelessWidget {
  10 + const WatchFacePreview({
  11 + super.key,
  12 + this.width = 134,
  13 + this.height = 160,
  14 + this.faceAsset,
  15 + this.faceFilePath,
  16 + });
  17 +
  18 + final double width;
  19 + final double height;
  20 + final String? faceAsset;
  21 + final String? faceFilePath;
  22 +
  23 + @override
  24 + Widget build(BuildContext context) {
  25 + final w = width.dp;
  26 + final h = height.dp;
  27 + final inset = 6.dp;
  28 + final knobWidth = (width * 0.067).dp;
  29 + final knobHeight = (height * 0.156).dp;
  30 +
  31 + return SizedBox(
  32 + width: w + knobWidth,
  33 + height: h,
  34 + child: Stack(
  35 + clipBehavior: Clip.none,
  36 + children: [
  37 + Positioned(
  38 + left: 0,
  39 + top: 0,
  40 + child: Container(
  41 + width: w,
  42 + height: h,
  43 + decoration: BoxDecoration(
  44 + color: WatchThemeColors.watchShell,
  45 + borderRadius: BorderRadius.circular((width * 0.239).dp),
  46 + ),
  47 + ),
  48 + ),
  49 + Positioned(
  50 + left: inset,
  51 + top: inset,
  52 + child: Container(
  53 + width: w - inset * 2,
  54 + height: h - inset * 2,
  55 + decoration: BoxDecoration(
  56 + color: WatchThemeColors.watchScreen,
  57 + borderRadius: BorderRadius.circular((width * 0.209).dp),
  58 + ),
  59 + clipBehavior: Clip.antiAlias,
  60 + child: _buildFaceImage(),
  61 + ),
  62 + ),
  63 + Positioned(
  64 + left: w - 4.dp,
  65 + top: (height * 0.2375).dp,
  66 + child: Container(
  67 + width: knobWidth,
  68 + height: knobHeight,
  69 + decoration: BoxDecoration(
  70 + color: WatchThemeColors.watchShell,
  71 + borderRadius: BorderRadius.horizontal(
  72 + right: Radius.circular(5.dp),
  73 + ),
  74 + ),
  75 + ),
  76 + ),
  77 + Positioned(
  78 + left: w - 1.dp,
  79 + top: (height * 0.5125).dp,
  80 + child: Container(
  81 + width: 3.dp,
  82 + height: (height * 0.256).dp,
  83 + decoration: BoxDecoration(
  84 + color: WatchThemeColors.watchShell,
  85 + borderRadius: BorderRadius.horizontal(
  86 + right: Radius.circular(2.dp),
  87 + ),
  88 + ),
  89 + ),
  90 + ),
  91 + ],
  92 + ),
  93 + );
  94 + }
  95 +
  96 + Widget _buildFaceImage() {
  97 + if (faceFilePath != null && faceFilePath!.isNotEmpty) {
  98 + return Image.file(File(faceFilePath!), fit: BoxFit.cover);
  99 + }
  100 + return Image.asset(
  101 + faceAsset ?? R.assetsImagesWatchThemeFaceDefault,
  102 + fit: BoxFit.cover,
  103 + );
  104 + }
  105 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_theme_colors.dart';
  5 +
  6 +class WatchThemeBottomActions extends StatelessWidget {
  7 + const WatchThemeBottomActions({
  8 + super.key,
  9 + required this.onAddTap,
  10 + this.secondaryLabel = '正在使用',
  11 + this.onSecondaryTap,
  12 + this.secondaryDisabled = true,
  13 + });
  14 +
  15 + final VoidCallback onAddTap;
  16 + final String secondaryLabel;
  17 + final VoidCallback? onSecondaryTap;
  18 + final bool secondaryDisabled;
  19 +
  20 + @override
  21 + Widget build(BuildContext context) {
  22 + return SafeArea(
  23 + top: false,
  24 + child: Container(
  25 + padding: EdgeInsets.fromLTRB(21.dp, 8.dp, 21.dp, 7.dp),
  26 + color: WatchThemeColors.background,
  27 + child: Row(
  28 + children: [
  29 + Expanded(
  30 + child: _ActionButton(
  31 + label: '添加表盘',
  32 + color: WatchThemeColors.brand,
  33 + textColor: Colors.white,
  34 + onTap: onAddTap,
  35 + ),
  36 + ),
  37 + SizedBox(width: 12.dp),
  38 + Expanded(
  39 + child: _ActionButton(
  40 + label: secondaryLabel,
  41 + color: secondaryDisabled
  42 + ? WatchThemeColors.brand.withValues(alpha: 0.4)
  43 + : WatchThemeColors.brand,
  44 + textColor:
  45 + secondaryDisabled ? WatchThemeColors.brand : Colors.white,
  46 + onTap: onSecondaryTap,
  47 + ),
  48 + ),
  49 + ],
  50 + ),
  51 + ),
  52 + );
  53 + }
  54 +}
  55 +
  56 +class _ActionButton extends StatelessWidget {
  57 + const _ActionButton({
  58 + required this.label,
  59 + required this.color,
  60 + required this.textColor,
  61 + required this.onTap,
  62 + });
  63 +
  64 + final String label;
  65 + final Color color;
  66 + final Color textColor;
  67 + final VoidCallback? onTap;
  68 +
  69 + @override
  70 + Widget build(BuildContext context) {
  71 + return GestureDetector(
  72 + behavior: HitTestBehavior.opaque,
  73 + onTap: onTap,
  74 + child: Container(
  75 + height: 48.dp,
  76 + alignment: Alignment.center,
  77 + decoration: BoxDecoration(
  78 + color: color,
  79 + borderRadius: BorderRadius.circular(24.dp),
  80 + ),
  81 + child: Text(
  82 + label,
  83 + style: TextStyle(
  84 + color: textColor,
  85 + fontSize: 16.dp,
  86 + fontWeight: FontWeight.w600,
  87 + height: 1.25,
  88 + ),
  89 + ),
  90 + ),
  91 + );
  92 + }
  93 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_theme_colors.dart';
  5 +
  6 +class WatchThemeCharacterAvatar extends StatelessWidget {
  7 + const WatchThemeCharacterAvatar({
  8 + super.key,
  9 + required this.size,
  10 + this.assetPath,
  11 + this.selected = false,
  12 + this.empty = false,
  13 + this.useDefaultCharacter = false,
  14 + this.backgroundColor = Colors.transparent,
  15 + });
  16 +
  17 + final double size;
  18 + final String? assetPath;
  19 + final bool selected;
  20 + final bool empty;
  21 + final bool useDefaultCharacter;
  22 + final Color backgroundColor;
  23 +
  24 + @override
  25 + Widget build(BuildContext context) {
  26 + final avatarSize = size.dp;
  27 + return SizedBox(
  28 + width: avatarSize,
  29 + height: avatarSize,
  30 + child: Stack(
  31 + clipBehavior: Clip.none,
  32 + children: [
  33 + Container(
  34 + width: avatarSize,
  35 + height: avatarSize,
  36 + decoration: BoxDecoration(
  37 + color: backgroundColor,
  38 + shape: BoxShape.circle,
  39 + border: selected
  40 + ? Border.all(color: WatchThemeColors.brand, width: 2.dp)
  41 + : null,
  42 + ),
  43 + alignment: Alignment.center,
  44 + child: empty
  45 + ? const SizedBox.shrink()
  46 + : useDefaultCharacter
  47 + ? assetPath == null
  48 + ? CustomPaint(
  49 + size: Size(avatarSize * 0.68, avatarSize * 0.68),
  50 + painter: const _DefaultCharacterPainter(),
  51 + )
  52 + : Image.asset(
  53 + assetPath!,
  54 + width: avatarSize * 0.82,
  55 + height: avatarSize * 0.82,
  56 + fit: BoxFit.contain,
  57 + )
  58 + : Image.asset(
  59 + assetPath!,
  60 + width: avatarSize * 0.82,
  61 + height: avatarSize * 0.82,
  62 + fit: BoxFit.contain,
  63 + ),
  64 + ),
  65 + if (selected)
  66 + Positioned(
  67 + right: -1.dp,
  68 + bottom: 9.dp,
  69 + child: Container(
  70 + width: 16.dp,
  71 + height: 16.dp,
  72 + decoration: const BoxDecoration(
  73 + color: WatchThemeColors.brand,
  74 + shape: BoxShape.circle,
  75 + ),
  76 + child: Icon(
  77 + Icons.check,
  78 + color: Colors.white,
  79 + size: 12.dp,
  80 + ),
  81 + ),
  82 + ),
  83 + ],
  84 + ),
  85 + );
  86 + }
  87 +}
  88 +
  89 +class _DefaultCharacterPainter extends CustomPainter {
  90 + const _DefaultCharacterPainter();
  91 +
  92 + @override
  93 + void paint(Canvas canvas, Size size) {
  94 + final body = Paint()..color = const Color(0xFF3DD79E);
  95 + final blush = Paint()..color = const Color(0xFFFF9A6E);
  96 + final dark = Paint()
  97 + ..color = const Color(0xFF153D2A)
  98 + ..strokeWidth = size.width * 0.045
  99 + ..strokeCap = StrokeCap.round
  100 + ..style = PaintingStyle.stroke;
  101 +
  102 + final path = Path()
  103 + ..moveTo(size.width * 0.32, size.height * 0.08)
  104 + ..cubicTo(size.width * 0.74, size.height * 0.02, size.width * 0.92,
  105 + size.height * 0.35, size.width * 0.82, size.height * 0.76)
  106 + ..cubicTo(size.width * 0.62, size.height * 1.03, size.width * 0.12,
  107 + size.height * 0.92, size.width * 0.10, size.height * 0.52)
  108 + ..cubicTo(size.width * 0.08, size.height * 0.28, size.width * 0.16,
  109 + size.height * 0.15, size.width * 0.32, size.height * 0.08)
  110 + ..close();
  111 + canvas.drawPath(path, body);
  112 + canvas.drawCircle(
  113 + Offset(size.width * 0.54, size.height * 0.55),
  114 + size.width * 0.09,
  115 + blush,
  116 + );
  117 + canvas.drawLine(
  118 + Offset(size.width * 0.28, size.height * 0.38),
  119 + Offset(size.width * 0.38, size.height * 0.36),
  120 + dark,
  121 + );
  122 + canvas.drawLine(
  123 + Offset(size.width * 0.59, size.height * 0.34),
  124 + Offset(size.width * 0.68, size.height * 0.31),
  125 + dark,
  126 + );
  127 + canvas.drawArc(
  128 + Rect.fromCenter(
  129 + center: Offset(size.width * 0.50, size.height * 0.55),
  130 + width: size.width * 0.22,
  131 + height: size.height * 0.14,
  132 + ),
  133 + 0.2,
  134 + 2.2,
  135 + false,
  136 + dark,
  137 + );
  138 + }
  139 +
  140 + @override
  141 + bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
  142 +}
  1 +import 'package:flutter/material.dart';
  2 +
  3 +class WatchThemeColors {
  4 + WatchThemeColors._();
  5 +
  6 + static const background = Color(0xFFF5F2FF);
  7 + static const gradientTop = Color(0xFFC5B0FF);
  8 + static const textPrimary = Color(0xFF0F0F11);
  9 + static const textSecondary = Color(0xFF78787D);
  10 + static const textTertiary = Color(0xFFB0B0B6);
  11 + static const brand = Color(0xFF845EEE);
  12 + static const card = Colors.white;
  13 + static const avatarBackground = Color(0xFFF4F1FE);
  14 + static const watchShell = Color(0xFF9388B3);
  15 + static const watchScreen = Color(0xFF0F0F11);
  16 + static const pro = Color(0xFFFFDF51);
  17 + static const excellent = Color(0xFF3BD49D);
  18 + static const normal = Color(0xFF7B9BFB);
  19 + static const stress = Color(0xFFFF9A6E);
  20 + static const overload = Color(0xFFFF5279);
  21 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_face_preview.dart';
  5 +import 'watch_theme_colors.dart';
  6 +
  7 +class WatchThemeHeader extends StatelessWidget {
  8 + const WatchThemeHeader({
  9 + super.key,
  10 + this.title = '默认主题',
  11 + this.faceAsset,
  12 + this.faceFilePath,
  13 + });
  14 +
  15 + final String title;
  16 + final String? faceAsset;
  17 + final String? faceFilePath;
  18 +
  19 + @override
  20 + Widget build(BuildContext context) {
  21 + return Column(
  22 + children: [
  23 + SizedBox(height: 12.dp),
  24 + Center(
  25 + child: WatchFacePreview(
  26 + faceAsset: faceAsset,
  27 + faceFilePath: faceFilePath,
  28 + ),
  29 + ),
  30 + SizedBox(height: 12.dp),
  31 + Text(
  32 + title,
  33 + style: TextStyle(
  34 + color: WatchThemeColors.textPrimary,
  35 + fontSize: 20.dp,
  36 + fontWeight: FontWeight.w600,
  37 + height: 1.25,
  38 + ),
  39 + ),
  40 + ],
  41 + );
  42 + }
  43 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:doublefeel_flutter/r.dart';
  3 +import 'package:flutter/material.dart';
  4 +import 'package:get/get.dart';
  5 +
  6 +import 'watch_theme_colors.dart';
  7 +
  8 +class WatchThemeNavBar extends StatelessWidget {
  9 + const WatchThemeNavBar({
  10 + super.key,
  11 + required this.title,
  12 + required this.onBack,
  13 + this.trailing,
  14 + });
  15 +
  16 + final String title;
  17 + final VoidCallback onBack;
  18 + final Widget? trailing;
  19 +
  20 + @override
  21 + Widget build(BuildContext context) {
  22 + return SizedBox(
  23 + height: 44.dp,
  24 + child: Row(
  25 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  26 + crossAxisAlignment: CrossAxisAlignment.center,
  27 + children: [
  28 + IconButton(
  29 + highlightColor: Colors.transparent,
  30 + splashColor: Colors.transparent,
  31 + padding: EdgeInsets.zero,
  32 + onPressed: onBack,
  33 + icon: Image.asset(
  34 + R.assetsImagesNavBackIcon,
  35 + width: 28.dp,
  36 + height: 28.dp,
  37 + ),
  38 + ),
  39 + Text(
  40 + title,
  41 + style: TextStyle(
  42 + color: WatchThemeColors.textPrimary,
  43 + fontSize: 16.dp,
  44 + fontWeight: FontWeight.w600,
  45 + height: 1.25,
  46 + ),
  47 + ),
  48 + trailing == null
  49 + ? SizedBox(
  50 + width: 8.dp,
  51 + )
  52 + : Center(child: trailing)
  53 + ],
  54 + ).paddingOnly(left: 8.dp, right: 8.dp),
  55 + );
  56 + }
  57 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_theme_colors.dart';
  5 +
  6 +class WatchThemeProBadge extends StatelessWidget {
  7 + const WatchThemeProBadge({super.key});
  8 +
  9 + @override
  10 + Widget build(BuildContext context) {
  11 + return Container(
  12 + height: 16.dp,
  13 + padding: EdgeInsets.only(left: 5.dp, right: 6.dp),
  14 + decoration: BoxDecoration(
  15 + color: WatchThemeColors.pro,
  16 + borderRadius: BorderRadius.circular(26.dp),
  17 + ),
  18 + child: Row(
  19 + mainAxisSize: MainAxisSize.min,
  20 + children: [
  21 + Icon(
  22 + Icons.workspace_premium,
  23 + color: WatchThemeColors.textPrimary,
  24 + size: 10.dp,
  25 + ),
  26 + SizedBox(width: 1.dp),
  27 + Text(
  28 + 'PRO',
  29 + style: TextStyle(
  30 + color: WatchThemeColors.textPrimary,
  31 + fontSize: 10.dp,
  32 + fontWeight: FontWeight.w600,
  33 + height: 1.1,
  34 + ),
  35 + ),
  36 + ],
  37 + ),
  38 + );
  39 + }
  40 +}
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'watch_theme_colors.dart';
  5 +
  6 +class WatchThemeSectionCard extends StatelessWidget {
  7 + const WatchThemeSectionCard({
  8 + super.key,
  9 + required this.child,
  10 + this.padding,
  11 + });
  12 +
  13 + final Widget child;
  14 + final EdgeInsetsGeometry? padding;
  15 +
  16 + @override
  17 + Widget build(BuildContext context) {
  18 + return Container(
  19 + width: double.infinity,
  20 + margin: EdgeInsets.symmetric(horizontal: 16.dp),
  21 + padding: padding ?? EdgeInsets.all(20.dp),
  22 + decoration: BoxDecoration(
  23 + color: WatchThemeColors.card,
  24 + borderRadius: BorderRadius.circular(16.dp),
  25 + ),
  26 + child: child,
  27 + );
  28 + }
  29 +}
  30 +
  31 +class WatchThemeSectionTitle extends StatelessWidget {
  32 + const WatchThemeSectionTitle(this.title, {super.key, this.trailing});
  33 +
  34 + final String title;
  35 + final Widget? trailing;
  36 +
  37 + @override
  38 + Widget build(BuildContext context) {
  39 + return Row(
  40 + mainAxisSize: MainAxisSize.min,
  41 + children: [
  42 + Text(
  43 + title,
  44 + style: TextStyle(
  45 + color: WatchThemeColors.textPrimary,
  46 + fontSize: 16.dp,
  47 + fontWeight: FontWeight.w500,
  48 + height: 1.25,
  49 + ),
  50 + ),
  51 + if (trailing != null) ...[
  52 + SizedBox(width: 4.dp),
  53 + trailing!,
  54 + ],
  55 + ],
  56 + );
  57 + }
  58 +}
@@ -24,6 +24,13 @@ import '../modules/splash/bindings/splash_binding.dart'; @@ -24,6 +24,13 @@ import '../modules/splash/bindings/splash_binding.dart';
24 import '../modules/splash/views/splash_page.dart'; 24 import '../modules/splash/views/splash_page.dart';
25 import '../modules/user_onboarding/bindings/user_onboarding_binding.dart'; 25 import '../modules/user_onboarding/bindings/user_onboarding_binding.dart';
26 import '../modules/user_onboarding/views/user_onboarding_view.dart'; 26 import '../modules/user_onboarding/views/user_onboarding_view.dart';
  27 +import '../modules/watch_theme/bindings/watch_theme_binding.dart';
  28 +import '../modules/watch_theme/views/custom_watch_theme_preview_view.dart';
  29 +import '../modules/watch_theme/views/create_watch_theme_view.dart';
  30 +import '../modules/watch_theme/views/watch_theme_preview_view.dart';
  31 +import '../modules/watch_theme/views/watch_theme_view.dart';
  32 +import '../modules/account_settings/bindings/account_settings_binding.dart';
  33 +import '../modules/account_settings/views/account_settings_view.dart';
27 import '../modules/webview/bindings/webview_binding.dart'; 34 import '../modules/webview/bindings/webview_binding.dart';
28 import '../modules/webview/views/webview_page.dart'; 35 import '../modules/webview/views/webview_page.dart';
29 36
@@ -116,5 +123,30 @@ abstract final class AppPages { @@ -116,5 +123,30 @@ abstract final class AppPages {
116 page: () => const PrivacySettingsView(), 123 page: () => const PrivacySettingsView(),
117 binding: PrivacySettingsBinding(), 124 binding: PrivacySettingsBinding(),
118 ), 125 ),
  126 + GetPage(
  127 + name: Routes.WATCH_THEME,
  128 + page: () => const WatchThemeView(),
  129 + binding: WatchThemeBinding(),
  130 + ),
  131 + GetPage(
  132 + name: Routes.WATCH_THEME_PREVIEW,
  133 + page: () => const WatchThemePreviewView(),
  134 + binding: WatchThemeBinding(),
  135 + ),
  136 + GetPage(
  137 + name: Routes.WATCH_THEME_CREATE,
  138 + page: () => const CreateWatchThemeView(),
  139 + binding: WatchThemeBinding(),
  140 + ),
  141 + GetPage(
  142 + name: Routes.WATCH_THEME_CUSTOM_PREVIEW,
  143 + page: () => const CustomWatchThemePreviewView(),
  144 + binding: WatchThemeBinding(),
  145 + ),
  146 + GetPage(
  147 + name: Routes.ACCOUNT_SETTINGS,
  148 + page: () => const AccountSettingsView(),
  149 + binding: AccountSettingsBinding(),
  150 + ),
119 ]; 151 ];
120 } 152 }
@@ -11,6 +11,11 @@ abstract class Routes { @@ -11,6 +11,11 @@ abstract class Routes {
11 static const SELECT_FRIEND = _Paths.SELECT_FRIEND; 11 static const SELECT_FRIEND = _Paths.SELECT_FRIEND;
12 static const PREMIUM_ACTIVATED = _Paths.PREMIUM_ACTIVATED; 12 static const PREMIUM_ACTIVATED = _Paths.PREMIUM_ACTIVATED;
13 static const PRIVACY_SETTINGS = _Paths.PRIVACY_SETTINGS; 13 static const PRIVACY_SETTINGS = _Paths.PRIVACY_SETTINGS;
  14 + static const WATCH_THEME = _Paths.WATCH_THEME;
  15 + static const WATCH_THEME_PREVIEW = _Paths.WATCH_THEME_PREVIEW;
  16 + static const WATCH_THEME_CREATE = _Paths.WATCH_THEME_CREATE;
  17 + static const WATCH_THEME_CUSTOM_PREVIEW = _Paths.WATCH_THEME_CUSTOM_PREVIEW;
  18 + static const ACCOUNT_SETTINGS = _Paths.ACCOUNT_SETTINGS;
14 } 19 }
15 20
16 abstract class _Paths { 21 abstract class _Paths {
@@ -23,4 +28,9 @@ abstract class _Paths { @@ -23,4 +28,9 @@ abstract class _Paths {
23 static const SELECT_FRIEND = '/select-friend'; 28 static const SELECT_FRIEND = '/select-friend';
24 static const PREMIUM_ACTIVATED = '/premium-activated'; 29 static const PREMIUM_ACTIVATED = '/premium-activated';
25 static const PRIVACY_SETTINGS = '/privacy-settings'; 30 static const PRIVACY_SETTINGS = '/privacy-settings';
  31 + static const WATCH_THEME = '/watch-theme';
  32 + static const WATCH_THEME_PREVIEW = '/watch-theme/preview';
  33 + static const WATCH_THEME_CREATE = '/watch-theme/create';
  34 + static const WATCH_THEME_CUSTOM_PREVIEW = '/watch-theme/custom-preview';
  35 + static const ACCOUNT_SETTINGS = '/account-settings';
26 } 36 }
@@ -15,21 +15,22 @@ PlatformException _createConnectionError(String channelName) { @@ -15,21 +15,22 @@ PlatformException _createConnectionError(String channelName) {
15 message: 'Unable to establish connection on channel: "$channelName".', 15 message: 'Unable to establish connection on channel: "$channelName".',
16 ); 16 );
17 } 17 }
  18 +
18 bool _deepEquals(Object? a, Object? b) { 19 bool _deepEquals(Object? a, Object? b) {
19 if (a is List && b is List) { 20 if (a is List && b is List) {
20 return a.length == b.length && 21 return a.length == b.length &&
21 a.indexed 22 a.indexed
22 - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); 23 + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
23 } 24 }
24 if (a is Map && b is Map) { 25 if (a is Map && b is Map) {
25 - return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>  
26 - (b as Map<Object?, Object?>).containsKey(entry.key) &&  
27 - _deepEquals(entry.value, b[entry.key])); 26 + return a.length == b.length &&
  27 + a.entries.every((MapEntry<Object?, Object?> entry) =>
  28 + (b as Map<Object?, Object?>).containsKey(entry.key) &&
  29 + _deepEquals(entry.value, b[entry.key]));
28 } 30 }
29 return a == b; 31 return a == b;
30 } 32 }
31 33
32 -  
33 class WearDeviceInfo { 34 class WearDeviceInfo {
34 WearDeviceInfo({ 35 WearDeviceInfo({
35 this.deviceId, 36 this.deviceId,
@@ -52,7 +53,8 @@ class WearDeviceInfo { @@ -52,7 +53,8 @@ class WearDeviceInfo {
52 } 53 }
53 54
54 Object encode() { 55 Object encode() {
55 - return _toList(); } 56 + return _toList();
  57 + }
56 58
57 static WearDeviceInfo decode(Object result) { 59 static WearDeviceInfo decode(Object result) {
58 result as List<Object?>; 60 result as List<Object?>;
@@ -77,11 +79,9 @@ class WearDeviceInfo { @@ -77,11 +79,9 @@ class WearDeviceInfo {
77 79
78 @override 80 @override
79 // ignore: avoid_equals_and_hash_code_on_mutable_classes 81 // ignore: avoid_equals_and_hash_code_on_mutable_classes
80 - int get hashCode => Object.hashAll(_toList())  
81 -; 82 + int get hashCode => Object.hashAll(_toList());
82 } 83 }
83 84
84 -  
85 class _PigeonCodec extends StandardMessageCodec { 85 class _PigeonCodec extends StandardMessageCodec {
86 const _PigeonCodec(); 86 const _PigeonCodec();
87 @override 87 @override
@@ -89,7 +89,7 @@ class _PigeonCodec extends StandardMessageCodec { @@ -89,7 +89,7 @@ class _PigeonCodec extends StandardMessageCodec {
89 if (value is int) { 89 if (value is int) {
90 buffer.putUint8(4); 90 buffer.putUint8(4);
91 buffer.putInt64(value); 91 buffer.putInt64(value);
92 - } else if (value is WearDeviceInfo) { 92 + } else if (value is WearDeviceInfo) {
93 buffer.putUint8(129); 93 buffer.putUint8(129);
94 writeValue(buffer, value.encode()); 94 writeValue(buffer, value.encode());
95 } else { 95 } else {
@@ -100,7 +100,7 @@ class _PigeonCodec extends StandardMessageCodec { @@ -100,7 +100,7 @@ class _PigeonCodec extends StandardMessageCodec {
100 @override 100 @override
101 Object? readValueOfType(int type, ReadBuffer buffer) { 101 Object? readValueOfType(int type, ReadBuffer buffer) {
102 switch (type) { 102 switch (type) {
103 - case 129: 103 + case 129:
104 return WearDeviceInfo.decode(readValue(buffer)!); 104 return WearDeviceInfo.decode(readValue(buffer)!);
105 default: 105 default:
106 return super.readValueOfType(type, buffer); 106 return super.readValueOfType(type, buffer);
@@ -112,9 +112,11 @@ class WearEngineHostApi { @@ -112,9 +112,11 @@ class WearEngineHostApi {
112 /// Constructor for [WearEngineHostApi]. The [binaryMessenger] named argument is 112 /// Constructor for [WearEngineHostApi]. The [binaryMessenger] named argument is
113 /// available for dependency injection. If it is left null, the default 113 /// available for dependency injection. If it is left null, the default
114 /// BinaryMessenger will be used which routes to the host platform. 114 /// BinaryMessenger will be used which routes to the host platform.
115 - WearEngineHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) 115 + WearEngineHostApi(
  116 + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
116 : pigeonVar_binaryMessenger = binaryMessenger, 117 : pigeonVar_binaryMessenger = binaryMessenger,
117 - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; 118 + pigeonVar_messageChannelSuffix =
  119 + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
118 final BinaryMessenger? pigeonVar_binaryMessenger; 120 final BinaryMessenger? pigeonVar_binaryMessenger;
119 121
120 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); 122 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -122,8 +124,10 @@ class WearEngineHostApi { @@ -122,8 +124,10 @@ class WearEngineHostApi {
122 final String pigeonVar_messageChannelSuffix; 124 final String pigeonVar_messageChannelSuffix;
123 125
124 Future<bool> hasAvailableDevices() async { 126 Future<bool> hasAvailableDevices() async {
125 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasAvailableDevices$pigeonVar_messageChannelSuffix';  
126 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 127 + final String pigeonVar_channelName =
  128 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasAvailableDevices$pigeonVar_messageChannelSuffix';
  129 + final BasicMessageChannel<Object?> pigeonVar_channel =
  130 + BasicMessageChannel<Object?>(
127 pigeonVar_channelName, 131 pigeonVar_channelName,
128 pigeonChannelCodec, 132 pigeonChannelCodec,
129 binaryMessenger: pigeonVar_binaryMessenger, 133 binaryMessenger: pigeonVar_binaryMessenger,
@@ -150,8 +154,10 @@ class WearEngineHostApi { @@ -150,8 +154,10 @@ class WearEngineHostApi {
150 } 154 }
151 155
152 Future<WearDeviceInfo?> checkConnectedDevice() async { 156 Future<WearDeviceInfo?> checkConnectedDevice() async {
153 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.checkConnectedDevice$pigeonVar_messageChannelSuffix';  
154 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 157 + final String pigeonVar_channelName =
  158 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.checkConnectedDevice$pigeonVar_messageChannelSuffix';
  159 + final BasicMessageChannel<Object?> pigeonVar_channel =
  160 + BasicMessageChannel<Object?>(
155 pigeonVar_channelName, 161 pigeonVar_channelName,
156 pigeonChannelCodec, 162 pigeonChannelCodec,
157 binaryMessenger: pigeonVar_binaryMessenger, 163 binaryMessenger: pigeonVar_binaryMessenger,
@@ -173,8 +179,10 @@ class WearEngineHostApi { @@ -173,8 +179,10 @@ class WearEngineHostApi {
173 } 179 }
174 180
175 Future<bool> registerMessageReceiver() async { 181 Future<bool> registerMessageReceiver() async {
176 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.registerMessageReceiver$pigeonVar_messageChannelSuffix';  
177 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 182 + final String pigeonVar_channelName =
  183 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.registerMessageReceiver$pigeonVar_messageChannelSuffix';
  184 + final BasicMessageChannel<Object?> pigeonVar_channel =
  185 + BasicMessageChannel<Object?>(
178 pigeonVar_channelName, 186 pigeonVar_channelName,
179 pigeonChannelCodec, 187 pigeonChannelCodec,
180 binaryMessenger: pigeonVar_binaryMessenger, 188 binaryMessenger: pigeonVar_binaryMessenger,
@@ -201,13 +209,16 @@ class WearEngineHostApi { @@ -201,13 +209,16 @@ class WearEngineHostApi {
201 } 209 }
202 210
203 Future<bool> sendTextMessage(String message) async { 211 Future<bool> sendTextMessage(String message) async {
204 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendTextMessage$pigeonVar_messageChannelSuffix';  
205 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 212 + final String pigeonVar_channelName =
  213 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendTextMessage$pigeonVar_messageChannelSuffix';
  214 + final BasicMessageChannel<Object?> pigeonVar_channel =
  215 + BasicMessageChannel<Object?>(
206 pigeonVar_channelName, 216 pigeonVar_channelName,
207 pigeonChannelCodec, 217 pigeonChannelCodec,
208 binaryMessenger: pigeonVar_binaryMessenger, 218 binaryMessenger: pigeonVar_binaryMessenger,
209 ); 219 );
210 - final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[message]); 220 + final Future<Object?> pigeonVar_sendFuture =
  221 + pigeonVar_channel.send(<Object?>[message]);
211 final List<Object?>? pigeonVar_replyList = 222 final List<Object?>? pigeonVar_replyList =
212 await pigeonVar_sendFuture as List<Object?>?; 223 await pigeonVar_sendFuture as List<Object?>?;
213 if (pigeonVar_replyList == null) { 224 if (pigeonVar_replyList == null) {
@@ -229,13 +240,16 @@ class WearEngineHostApi { @@ -229,13 +240,16 @@ class WearEngineHostApi {
229 } 240 }
230 241
231 Future<bool> sendWatchSyncPayload(String jsonPayload) async { 242 Future<bool> sendWatchSyncPayload(String jsonPayload) async {
232 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix';  
233 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 243 + final String pigeonVar_channelName =
  244 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix';
  245 + final BasicMessageChannel<Object?> pigeonVar_channel =
  246 + BasicMessageChannel<Object?>(
234 pigeonVar_channelName, 247 pigeonVar_channelName,
235 pigeonChannelCodec, 248 pigeonChannelCodec,
236 binaryMessenger: pigeonVar_binaryMessenger, 249 binaryMessenger: pigeonVar_binaryMessenger,
237 ); 250 );
238 - final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[jsonPayload]); 251 + final Future<Object?> pigeonVar_sendFuture =
  252 + pigeonVar_channel.send(<Object?>[jsonPayload]);
239 final List<Object?>? pigeonVar_replyList = 253 final List<Object?>? pigeonVar_replyList =
240 await pigeonVar_sendFuture as List<Object?>?; 254 await pigeonVar_sendFuture as List<Object?>?;
241 if (pigeonVar_replyList == null) { 255 if (pigeonVar_replyList == null) {
@@ -255,4 +269,29 @@ class WearEngineHostApi { @@ -255,4 +269,29 @@ class WearEngineHostApi {
255 return (pigeonVar_replyList[0] as bool?)!; 269 return (pigeonVar_replyList[0] as bool?)!;
256 } 270 }
257 } 271 }
  272 +
  273 + Future<String?> pickImageAndRemoveBackground() async {
  274 + final String pigeonVar_channelName =
  275 + 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground$pigeonVar_messageChannelSuffix';
  276 + final BasicMessageChannel<Object?> pigeonVar_channel =
  277 + BasicMessageChannel<Object?>(
  278 + pigeonVar_channelName,
  279 + pigeonChannelCodec,
  280 + binaryMessenger: pigeonVar_binaryMessenger,
  281 + );
  282 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  283 + final List<Object?>? pigeonVar_replyList =
  284 + await pigeonVar_sendFuture as List<Object?>?;
  285 + if (pigeonVar_replyList == null) {
  286 + throw _createConnectionError(pigeonVar_channelName);
  287 + } else if (pigeonVar_replyList.length > 1) {
  288 + throw PlatformException(
  289 + code: pigeonVar_replyList[0]! as String,
  290 + message: pigeonVar_replyList[1] as String?,
  291 + details: pigeonVar_replyList[2],
  292 + );
  293 + } else {
  294 + return (pigeonVar_replyList[0] as String?);
  295 + }
  296 + }
258 } 297 }
1 class R { 1 class R {
2 - static final String assetsImagesNavBackIcon = 'assets/images/common/ic_nav_back.webp';  
3 - static final String assetsImagesHealthGoodArrow = 'assets/images/common/ic_health_good_arrow.webp';  
4 - static final String assetsImagesHealthBadArrow = 'assets/images/common/ic_health_bad_arrow.webp';  
5 - static final String assetsImagesHealthTrendUp = 'assets/images/common/ic_health_trend_up.webp';  
6 - static final String assetsImagesHealthTrendDown = 'assets/images/common/ic_health_trend_down.webp';  
7 - static final String assetsImagesHealthFriendSleepQuality = 'assets/images/common/ic_health_friend_sleep_quality.webp';  
8 - static final String assetsImagesHealthFriendSteps = 'assets/images/common/ic_health_friend_steps.webp';  
9 - static final String assetsImagesSettings = 'assets/images/common/ic_settings.png';  
10 - static final String assetsImagesCopyPurple = 'assets/images/common/ic_copy_purple.webp'; 2 + static final String assetsImagesNavBackIcon =
  3 + 'assets/images/common/ic_nav_back.webp';
  4 + static final String assetsImagesHealthGoodArrow =
  5 + 'assets/images/common/ic_health_good_arrow.webp';
  6 + static final String assetsImagesHealthBadArrow =
  7 + 'assets/images/common/ic_health_bad_arrow.webp';
  8 + static final String assetsImagesHealthTrendUp =
  9 + 'assets/images/common/ic_health_trend_up.webp';
  10 + static final String assetsImagesHealthTrendDown =
  11 + 'assets/images/common/ic_health_trend_down.webp';
  12 + static final String assetsImagesHealthFriendSleepQuality =
  13 + 'assets/images/common/ic_health_friend_sleep_quality.webp';
  14 + static final String assetsImagesHealthFriendSteps =
  15 + 'assets/images/common/ic_health_friend_steps.webp';
  16 + static final String assetsImagesSettings =
  17 + 'assets/images/common/ic_settings.png';
  18 + static final String assetsImagesCopyPurple =
  19 + 'assets/images/common/ic_copy_purple.webp';
11 static final String assetsImagesProIcon = 'assets/images/common/ic_pro.webp'; 20 static final String assetsImagesProIcon = 'assets/images/common/ic_pro.webp';
12 21
13 // friends 22 // friends
14 - static final String assetsImagesFriendsFriendAddMeIllustration = 'assets/images/friends/ic_friend_add_me_illustration.webp';  
15 - static final String assetsImagesFriendsFriendAddOtherIllustration = 'assets/images/friends/ic_friend_add_other_illustration.webp';  
16 - static final String assetsImagesFriendsFriendAddSubtractBg = 'assets/images/friends/bg_friend_add_subtract.webp';  
17 - static final String assetsImagesFriendsFriendAddVectorIcon = 'assets/images/friends/ic_friend_add_vector.webp'; 23 + static final String assetsImagesFriendsFriendAddMeIllustration =
  24 + 'assets/images/friends/ic_friend_add_me_illustration.webp';
  25 + static final String assetsImagesFriendsFriendAddOtherIllustration =
  26 + 'assets/images/friends/ic_friend_add_other_illustration.webp';
  27 + static final String assetsImagesFriendsFriendAddSubtractBg =
  28 + 'assets/images/friends/bg_friend_add_subtract.webp';
  29 + static final String assetsImagesFriendsFriendAddVectorIcon =
  30 + 'assets/images/friends/ic_friend_add_vector.webp';
  31 +
  32 + // my
  33 + static final String assetsImagesMyAvatarDefault =
  34 + 'assets/images/my/avatar_default.png';
  35 +
  36 + // watch theme
  37 + static final String assetsImagesWatchThemeFaceDefault =
  38 + 'assets/images/watch_theme/watch_face_default.png';
  39 + static final String assetsImagesWatchThemeFacePreviewAlt =
  40 + 'assets/images/watch_theme/watch_face_preview_alt.png';
  41 + static final String assetsImagesWatchThemeCustomFaceCreate =
  42 + 'assets/images/watch_theme/custom_watch_face_create.png';
  43 + static final String assetsImagesWatchThemeCustomFacePreview =
  44 + 'assets/images/watch_theme/custom_watch_face_preview.png';
  45 + static final String assetsImagesWatchThemeCustomPreviewAlt =
  46 + 'assets/images/watch_theme/custom_preview_alt.png';
  47 + static final String assetsImagesWatchThemeCustomStatusExcellent =
  48 + 'assets/images/watch_theme/custom_status_excellent.png';
  49 + static final String assetsImagesWatchThemeCustomStatusNormal =
  50 + 'assets/images/watch_theme/custom_status_normal.png';
  51 + static final String assetsImagesWatchThemeCustomStatusStress =
  52 + 'assets/images/watch_theme/custom_status_stress.png';
  53 + static final String assetsImagesWatchThemeCustomStatusOverload =
  54 + 'assets/images/watch_theme/custom_status_overload.png';
  55 + static final String assetsImagesWatchThemeOfficialDogWhite =
  56 + 'assets/images/watch_theme/official_dog_white.png';
  57 + static final String assetsImagesWatchThemeOfficialRabbitPink =
  58 + 'assets/images/watch_theme/official_rabbit_pink.png';
  59 + static final String assetsImagesWatchThemeOfficialCatOrange =
  60 + 'assets/images/watch_theme/official_cat_orange.png';
  61 + static final String assetsImagesWatchThemeOfficialElephantBlue =
  62 + 'assets/images/watch_theme/official_elephant_blue.png';
  63 + static final String assetsImagesWatchThemeCustomCatDog =
  64 + 'assets/images/watch_theme/custom_cat_dog.png';
18 } 65 }
@@ -39,4 +39,8 @@ abstract class WearEngineHostApi { @@ -39,4 +39,8 @@ abstract class WearEngineHostApi {
39 bool sendTextMessage(String message); 39 bool sendTextMessage(String message);
40 40
41 bool sendWatchSyncPayload(String jsonPayload); 41 bool sendWatchSyncPayload(String jsonPayload);
  42 +
  43 + /// Opens the system photo picker and returns a local PNG file path after
  44 + /// removing the image background on the host platform.
  45 + String? pickImageAndRemoveBackground();
42 } 46 }
@@ -69,3 +69,5 @@ flutter: @@ -69,3 +69,5 @@ flutter:
69 - assets/images/tabbar/ 69 - assets/images/tabbar/
70 - assets/images/today/ 70 - assets/images/today/
71 - assets/images/friends/ 71 - assets/images/friends/
  72 + - assets/images/my/
  73 + - assets/images/watch_theme/