Showing
53 changed files
with
3221 additions
and
80 deletions
assets/images/my/avatar_default.png
0 → 100644
44.3 KB
assets/images/watch_theme/custom_cat_dog.png
0 → 100644
50 KB
21.9 KB
1.15 MB
832 KB
1.11 MB
692 KB
63.1 KB
63.1 KB
9.38 KB
6.42 KB
6.42 KB
6.26 KB
6.39 KB
9.73 KB
12.7 KB
11.7 KB
63.1 KB
21.9 KB
| @@ -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: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 | } |
-
Please register or login to post a comment