Commit 87e39b965578ac2ab9ee475cf30a79d698bf109c
1 parent
cb2e56ad
feat(ui):增加bridge一些接口,增加原生的debug环境回传flutter(切环境)
Showing
48 changed files
with
768 additions
and
344 deletions
| @@ -54,6 +54,12 @@ | @@ -54,6 +54,12 @@ | ||
| 54 | @import sqflite_darwin; | 54 | @import sqflite_darwin; |
| 55 | #endif | 55 | #endif |
| 56 | 56 | ||
| 57 | +#if __has_include(<video_thumbnail/VideoThumbnailPlugin.h>) | ||
| 58 | +#import <video_thumbnail/VideoThumbnailPlugin.h> | ||
| 59 | +#else | ||
| 60 | +@import video_thumbnail; | ||
| 61 | +#endif | ||
| 62 | + | ||
| 57 | #if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>) | 63 | #if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>) |
| 58 | #import <webview_flutter_wkwebview/WebViewFlutterPlugin.h> | 64 | #import <webview_flutter_wkwebview/WebViewFlutterPlugin.h> |
| 59 | #else | 65 | #else |
| @@ -71,6 +77,7 @@ | @@ -71,6 +77,7 @@ | ||
| 71 | [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; | 77 | [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; |
| 72 | [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; | 78 | [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; |
| 73 | [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; | 79 | [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; |
| 80 | + [VideoThumbnailPlugin registerWithRegistrar:[registry registrarForPlugin:@"VideoThumbnailPlugin"]]; | ||
| 74 | [WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]]; | 81 | [WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]]; |
| 75 | } | 82 | } |
| 76 | 83 |
| @@ -79,6 +79,17 @@ class FlutterError ( | @@ -79,6 +79,17 @@ class FlutterError ( | ||
| 79 | val details: Any? = null | 79 | val details: Any? = null |
| 80 | ) : Throwable() | 80 | ) : Throwable() |
| 81 | 81 | ||
| 82 | +enum class HResourceType(val raw: Int) { | ||
| 83 | + IMAGE(0), | ||
| 84 | + VIDEO(1); | ||
| 85 | + | ||
| 86 | + companion object { | ||
| 87 | + fun ofRaw(raw: Int): HResourceType? { | ||
| 88 | + return values().firstOrNull { it.raw == raw } | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | +} | ||
| 92 | + | ||
| 82 | /** Generated class from Pigeon that represents data sent in messages. */ | 93 | /** Generated class from Pigeon that represents data sent in messages. */ |
| 83 | data class AppleSignInModel ( | 94 | data class AppleSignInModel ( |
| 84 | val userId: String, | 95 | val userId: String, |
| @@ -265,21 +276,26 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { | @@ -265,21 +276,26 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { | ||
| 265 | override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { | 276 | override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { |
| 266 | return when (type) { | 277 | return when (type) { |
| 267 | 129.toByte() -> { | 278 | 129.toByte() -> { |
| 279 | + return (readValue(buffer) as Long?)?.let { | ||
| 280 | + HResourceType.ofRaw(it.toInt()) | ||
| 281 | + } | ||
| 282 | + } | ||
| 283 | + 130.toByte() -> { | ||
| 268 | return (readValue(buffer) as? List<Any?>)?.let { | 284 | return (readValue(buffer) as? List<Any?>)?.let { |
| 269 | AppleSignInModel.fromList(it) | 285 | AppleSignInModel.fromList(it) |
| 270 | } | 286 | } |
| 271 | } | 287 | } |
| 272 | - 130.toByte() -> { | 288 | + 131.toByte() -> { |
| 273 | return (readValue(buffer) as? List<Any?>)?.let { | 289 | return (readValue(buffer) as? List<Any?>)?.let { |
| 274 | WatchAppOtherInfo.fromList(it) | 290 | WatchAppOtherInfo.fromList(it) |
| 275 | } | 291 | } |
| 276 | } | 292 | } |
| 277 | - 131.toByte() -> { | 293 | + 132.toByte() -> { |
| 278 | return (readValue(buffer) as? List<Any?>)?.let { | 294 | return (readValue(buffer) as? List<Any?>)?.let { |
| 279 | AppleProductInfo.fromList(it) | 295 | AppleProductInfo.fromList(it) |
| 280 | } | 296 | } |
| 281 | } | 297 | } |
| 282 | - 132.toByte() -> { | 298 | + 133.toByte() -> { |
| 283 | return (readValue(buffer) as? List<Any?>)?.let { | 299 | return (readValue(buffer) as? List<Any?>)?.let { |
| 284 | AppleProductPaymentResult.fromList(it) | 300 | AppleProductPaymentResult.fromList(it) |
| 285 | } | 301 | } |
| @@ -289,20 +305,24 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { | @@ -289,20 +305,24 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() { | ||
| 289 | } | 305 | } |
| 290 | override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { | 306 | override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { |
| 291 | when (value) { | 307 | when (value) { |
| 292 | - is AppleSignInModel -> { | 308 | + is HResourceType -> { |
| 293 | stream.write(129) | 309 | stream.write(129) |
| 310 | + writeValue(stream, value.raw) | ||
| 311 | + } | ||
| 312 | + is AppleSignInModel -> { | ||
| 313 | + stream.write(130) | ||
| 294 | writeValue(stream, value.toList()) | 314 | writeValue(stream, value.toList()) |
| 295 | } | 315 | } |
| 296 | is WatchAppOtherInfo -> { | 316 | is WatchAppOtherInfo -> { |
| 297 | - stream.write(130) | 317 | + stream.write(131) |
| 298 | writeValue(stream, value.toList()) | 318 | writeValue(stream, value.toList()) |
| 299 | } | 319 | } |
| 300 | is AppleProductInfo -> { | 320 | is AppleProductInfo -> { |
| 301 | - stream.write(131) | 321 | + stream.write(132) |
| 302 | writeValue(stream, value.toList()) | 322 | writeValue(stream, value.toList()) |
| 303 | } | 323 | } |
| 304 | is AppleProductPaymentResult -> { | 324 | is AppleProductPaymentResult -> { |
| 305 | - stream.write(132) | 325 | + stream.write(133) |
| 306 | writeValue(stream, value.toList()) | 326 | writeValue(stream, value.toList()) |
| 307 | } | 327 | } |
| 308 | else -> super.writeValue(stream, value) | 328 | else -> super.writeValue(stream, value) |
| @@ -318,6 +338,8 @@ interface PlatformHostApi { | @@ -318,6 +338,8 @@ interface PlatformHostApi { | ||
| 318 | * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 338 | * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 319 | */ | 339 | */ |
| 320 | fun getFullUserAgent(): String | 340 | fun getFullUserAgent(): String |
| 341 | + /** 是否是测试环境 */ | ||
| 342 | + fun isDebugEnvoriment(): Boolean | ||
| 321 | /** | 343 | /** |
| 322 | * 更新用户信息, 有登录态后调用 | 344 | * 更新用户信息, 有登录态后调用 |
| 323 | * jsonString: UserPreferences的序列化string | 345 | * jsonString: UserPreferences的序列化string |
| @@ -329,10 +351,12 @@ interface PlatformHostApi { | @@ -329,10 +351,12 @@ interface PlatformHostApi { | ||
| 329 | /** 刷新会员信息 */ | 351 | /** 刷新会员信息 */ |
| 330 | fun refreshVip() | 352 | fun refreshVip() |
| 331 | /** | 353 | /** |
| 332 | - * 刷新watch app 和 表盘的所有数据: | 354 | + * 刷新watch app 和 表盘的所有数据: |
| 333 | * 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) | 355 | * 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) |
| 334 | */ | 356 | */ |
| 335 | fun refreshWatchAppAndWidgets() | 357 | fun refreshWatchAppAndWidgets() |
| 358 | + /** 请求评分弹窗 */ | ||
| 359 | + fun requestAppReview(callback: (Result<Boolean>) -> Unit) | ||
| 336 | /** 请求苹果登录 */ | 360 | /** 请求苹果登录 */ |
| 337 | fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit) | 361 | fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit) |
| 338 | /** | 362 | /** |
| @@ -345,6 +369,11 @@ interface PlatformHostApi { | @@ -345,6 +369,11 @@ interface PlatformHostApi { | ||
| 345 | fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit) | 369 | fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit) |
| 346 | /** 恢复购买 */ | 370 | /** 恢复购买 */ |
| 347 | fun performRestore(callback: (Result<Boolean>) -> Unit) | 371 | fun performRestore(callback: (Result<Boolean>) -> Unit) |
| 372 | + /** | ||
| 373 | + * 上传文件到云端 | ||
| 374 | + * 注意catch flutter error | ||
| 375 | + */ | ||
| 376 | + fun uploadFile(filePath: String, resourceType: HResourceType, callback: (Result<String?>) -> Unit) | ||
| 348 | 377 | ||
| 349 | companion object { | 378 | companion object { |
| 350 | /** The codec used by PlatformHostApi. */ | 379 | /** The codec used by PlatformHostApi. */ |
| @@ -371,6 +400,21 @@ interface PlatformHostApi { | @@ -371,6 +400,21 @@ interface PlatformHostApi { | ||
| 371 | } | 400 | } |
| 372 | } | 401 | } |
| 373 | run { | 402 | run { |
| 403 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$separatedMessageChannelSuffix", codec) | ||
| 404 | + if (api != null) { | ||
| 405 | + channel.setMessageHandler { _, reply -> | ||
| 406 | + val wrapped: List<Any?> = try { | ||
| 407 | + listOf(api.isDebugEnvoriment()) | ||
| 408 | + } catch (exception: Throwable) { | ||
| 409 | + PlatformApiPigeonUtils.wrapError(exception) | ||
| 410 | + } | ||
| 411 | + reply.reply(wrapped) | ||
| 412 | + } | ||
| 413 | + } else { | ||
| 414 | + channel.setMessageHandler(null) | ||
| 415 | + } | ||
| 416 | + } | ||
| 417 | + run { | ||
| 374 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec) | 418 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec) |
| 375 | if (api != null) { | 419 | if (api != null) { |
| 376 | channel.setMessageHandler { message, reply -> | 420 | channel.setMessageHandler { message, reply -> |
| @@ -438,6 +482,24 @@ interface PlatformHostApi { | @@ -438,6 +482,24 @@ interface PlatformHostApi { | ||
| 438 | } | 482 | } |
| 439 | } | 483 | } |
| 440 | run { | 484 | run { |
| 485 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$separatedMessageChannelSuffix", codec) | ||
| 486 | + if (api != null) { | ||
| 487 | + channel.setMessageHandler { _, reply -> | ||
| 488 | + api.requestAppReview{ result: Result<Boolean> -> | ||
| 489 | + val error = result.exceptionOrNull() | ||
| 490 | + if (error != null) { | ||
| 491 | + reply.reply(PlatformApiPigeonUtils.wrapError(error)) | ||
| 492 | + } else { | ||
| 493 | + val data = result.getOrNull() | ||
| 494 | + reply.reply(PlatformApiPigeonUtils.wrapResult(data)) | ||
| 495 | + } | ||
| 496 | + } | ||
| 497 | + } | ||
| 498 | + } else { | ||
| 499 | + channel.setMessageHandler(null) | ||
| 500 | + } | ||
| 501 | + } | ||
| 502 | + run { | ||
| 441 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$separatedMessageChannelSuffix", codec) | 503 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$separatedMessageChannelSuffix", codec) |
| 442 | if (api != null) { | 504 | if (api != null) { |
| 443 | channel.setMessageHandler { _, reply -> | 505 | channel.setMessageHandler { _, reply -> |
| @@ -515,6 +577,27 @@ interface PlatformHostApi { | @@ -515,6 +577,27 @@ interface PlatformHostApi { | ||
| 515 | channel.setMessageHandler(null) | 577 | channel.setMessageHandler(null) |
| 516 | } | 578 | } |
| 517 | } | 579 | } |
| 580 | + run { | ||
| 581 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$separatedMessageChannelSuffix", codec) | ||
| 582 | + if (api != null) { | ||
| 583 | + channel.setMessageHandler { message, reply -> | ||
| 584 | + val args = message as List<Any?> | ||
| 585 | + val filePathArg = args[0] as String | ||
| 586 | + val resourceTypeArg = args[1] as HResourceType | ||
| 587 | + api.uploadFile(filePathArg, resourceTypeArg) { result: Result<String?> -> | ||
| 588 | + val error = result.exceptionOrNull() | ||
| 589 | + if (error != null) { | ||
| 590 | + reply.reply(PlatformApiPigeonUtils.wrapError(error)) | ||
| 591 | + } else { | ||
| 592 | + val data = result.getOrNull() | ||
| 593 | + reply.reply(PlatformApiPigeonUtils.wrapResult(data)) | ||
| 594 | + } | ||
| 595 | + } | ||
| 596 | + } | ||
| 597 | + } else { | ||
| 598 | + channel.setMessageHandler(null) | ||
| 599 | + } | ||
| 600 | + } | ||
| 518 | } | 601 | } |
| 519 | } | 602 | } |
| 520 | } | 603 | } |
| @@ -147,6 +147,8 @@ interface WearEngineHostApi { | @@ -147,6 +147,8 @@ interface WearEngineHostApi { | ||
| 147 | * removing the image background on the host platform. | 147 | * removing the image background on the host platform. |
| 148 | */ | 148 | */ |
| 149 | fun removeBackground(originImagePath: String, callback: (Result<String?>) -> Unit) | 149 | fun removeBackground(originImagePath: String, callback: (Result<String?>) -> Unit) |
| 150 | + /** 是否有已安装的表盘 */ | ||
| 151 | + fun hasInstalledWatchSurface(callback: (Result<Boolean>) -> Unit) | ||
| 150 | 152 | ||
| 151 | companion object { | 153 | companion object { |
| 152 | /** The codec used by WearEngineHostApi. */ | 154 | /** The codec used by WearEngineHostApi. */ |
| @@ -256,6 +258,24 @@ interface WearEngineHostApi { | @@ -256,6 +258,24 @@ interface WearEngineHostApi { | ||
| 256 | channel.setMessageHandler(null) | 258 | channel.setMessageHandler(null) |
| 257 | } | 259 | } |
| 258 | } | 260 | } |
| 261 | + run { | ||
| 262 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$separatedMessageChannelSuffix", codec) | ||
| 263 | + if (api != null) { | ||
| 264 | + channel.setMessageHandler { _, reply -> | ||
| 265 | + api.hasInstalledWatchSurface{ result: Result<Boolean> -> | ||
| 266 | + val error = result.exceptionOrNull() | ||
| 267 | + if (error != null) { | ||
| 268 | + reply.reply(WearEngineApiPigeonUtils.wrapError(error)) | ||
| 269 | + } else { | ||
| 270 | + val data = result.getOrNull() | ||
| 271 | + reply.reply(WearEngineApiPigeonUtils.wrapResult(data)) | ||
| 272 | + } | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | + } else { | ||
| 276 | + channel.setMessageHandler(null) | ||
| 277 | + } | ||
| 278 | + } | ||
| 259 | } | 279 | } |
| 260 | } | 280 | } |
| 261 | } | 281 | } |
50 KB
21.9 KB
1.15 MB
832 KB
1.11 MB
692 KB
63.1 KB
63.1 KB
9.38 KB
9.73 KB
12.7 KB
11.7 KB
63.1 KB
21.9 KB
8.9 KB
21.4 KB
9.89 KB
| @@ -112,6 +112,11 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) { | @@ -112,6 +112,11 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) { | ||
| 112 | 112 | ||
| 113 | 113 | ||
| 114 | 114 | ||
| 115 | +enum HResourceType: Int { | ||
| 116 | + case image = 0 | ||
| 117 | + case video = 1 | ||
| 118 | +} | ||
| 119 | + | ||
| 115 | /// Generated class from Pigeon that represents data sent in messages. | 120 | /// Generated class from Pigeon that represents data sent in messages. |
| 116 | struct AppleSignInModel: Hashable { | 121 | struct AppleSignInModel: Hashable { |
| 117 | var userId: String | 122 | var userId: String |
| @@ -301,12 +306,18 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { | @@ -301,12 +306,18 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { | ||
| 301 | override func readValue(ofType type: UInt8) -> Any? { | 306 | override func readValue(ofType type: UInt8) -> Any? { |
| 302 | switch type { | 307 | switch type { |
| 303 | case 129: | 308 | case 129: |
| 304 | - return AppleSignInModel.fromList(self.readValue() as! [Any?]) | 309 | + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) |
| 310 | + if let enumResultAsInt = enumResultAsInt { | ||
| 311 | + return HResourceType(rawValue: enumResultAsInt) | ||
| 312 | + } | ||
| 313 | + return nil | ||
| 305 | case 130: | 314 | case 130: |
| 306 | - return WatchAppOtherInfo.fromList(self.readValue() as! [Any?]) | 315 | + return AppleSignInModel.fromList(self.readValue() as! [Any?]) |
| 307 | case 131: | 316 | case 131: |
| 308 | - return AppleProductInfo.fromList(self.readValue() as! [Any?]) | 317 | + return WatchAppOtherInfo.fromList(self.readValue() as! [Any?]) |
| 309 | case 132: | 318 | case 132: |
| 319 | + return AppleProductInfo.fromList(self.readValue() as! [Any?]) | ||
| 320 | + case 133: | ||
| 310 | return AppleProductPaymentResult.fromList(self.readValue() as! [Any?]) | 321 | return AppleProductPaymentResult.fromList(self.readValue() as! [Any?]) |
| 311 | default: | 322 | default: |
| 312 | return super.readValue(ofType: type) | 323 | return super.readValue(ofType: type) |
| @@ -316,17 +327,20 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { | @@ -316,17 +327,20 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader { | ||
| 316 | 327 | ||
| 317 | private class PlatformApiPigeonCodecWriter: FlutterStandardWriter { | 328 | private class PlatformApiPigeonCodecWriter: FlutterStandardWriter { |
| 318 | override func writeValue(_ value: Any) { | 329 | override func writeValue(_ value: Any) { |
| 319 | - if let value = value as? AppleSignInModel { | 330 | + if let value = value as? HResourceType { |
| 320 | super.writeByte(129) | 331 | super.writeByte(129) |
| 332 | + super.writeValue(value.rawValue) | ||
| 333 | + } else if let value = value as? AppleSignInModel { | ||
| 334 | + super.writeByte(130) | ||
| 321 | super.writeValue(value.toList()) | 335 | super.writeValue(value.toList()) |
| 322 | } else if let value = value as? WatchAppOtherInfo { | 336 | } else if let value = value as? WatchAppOtherInfo { |
| 323 | - super.writeByte(130) | 337 | + super.writeByte(131) |
| 324 | super.writeValue(value.toList()) | 338 | super.writeValue(value.toList()) |
| 325 | } else if let value = value as? AppleProductInfo { | 339 | } else if let value = value as? AppleProductInfo { |
| 326 | - super.writeByte(131) | 340 | + super.writeByte(132) |
| 327 | super.writeValue(value.toList()) | 341 | super.writeValue(value.toList()) |
| 328 | } else if let value = value as? AppleProductPaymentResult { | 342 | } else if let value = value as? AppleProductPaymentResult { |
| 329 | - super.writeByte(132) | 343 | + super.writeByte(133) |
| 330 | super.writeValue(value.toList()) | 344 | super.writeValue(value.toList()) |
| 331 | } else { | 345 | } else { |
| 332 | super.writeValue(value) | 346 | super.writeValue(value) |
| @@ -354,6 +368,8 @@ protocol PlatformHostApi { | @@ -354,6 +368,8 @@ protocol PlatformHostApi { | ||
| 354 | /// 返回完整的 User-Agent 字符串,由 native 侧组装: | 368 | /// 返回完整的 User-Agent 字符串,由 native 侧组装: |
| 355 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 369 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 356 | func getFullUserAgent() throws -> String | 370 | func getFullUserAgent() throws -> String |
| 371 | + /// 是否是测试环境 | ||
| 372 | + func isDebugEnvoriment() throws -> Bool | ||
| 357 | /// 更新用户信息, 有登录态后调用 | 373 | /// 更新用户信息, 有登录态后调用 |
| 358 | /// jsonString: UserPreferences的序列化string | 374 | /// jsonString: UserPreferences的序列化string |
| 359 | /// baseUrl: 请求地址, https://api.doublefeel.cn | 375 | /// baseUrl: 请求地址, https://api.doublefeel.cn |
| @@ -362,9 +378,11 @@ protocol PlatformHostApi { | @@ -362,9 +378,11 @@ protocol PlatformHostApi { | ||
| 362 | func logout() throws | 378 | func logout() throws |
| 363 | /// 刷新会员信息 | 379 | /// 刷新会员信息 |
| 364 | func refreshVip() throws | 380 | func refreshVip() throws |
| 365 | - /// 刷新watch app 和 表盘的所有数据: | 381 | + /// 刷新watch app 和 表盘的所有数据: |
| 366 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) | 382 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) |
| 367 | func refreshWatchAppAndWidgets() throws | 383 | func refreshWatchAppAndWidgets() throws |
| 384 | + /// 请求评分弹窗 | ||
| 385 | + func requestAppReview(completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 368 | /// 请求苹果登录 | 386 | /// 请求苹果登录 |
| 369 | func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void) | 387 | func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void) |
| 370 | /// 查询指定id的苹果商品 | 388 | /// 查询指定id的苹果商品 |
| @@ -375,6 +393,9 @@ protocol PlatformHostApi { | @@ -375,6 +393,9 @@ protocol PlatformHostApi { | ||
| 375 | func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void) | 393 | func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void) |
| 376 | /// 恢复购买 | 394 | /// 恢复购买 |
| 377 | func performRestore(completion: @escaping (Result<Bool, Error>) -> Void) | 395 | func performRestore(completion: @escaping (Result<Bool, Error>) -> Void) |
| 396 | + /// 上传文件到云端 | ||
| 397 | + /// 注意catch flutter error | ||
| 398 | + func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, Error>) -> Void) | ||
| 378 | } | 399 | } |
| 379 | 400 | ||
| 380 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | 401 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. |
| @@ -398,6 +419,20 @@ class PlatformHostApiSetup { | @@ -398,6 +419,20 @@ class PlatformHostApiSetup { | ||
| 398 | } else { | 419 | } else { |
| 399 | getFullUserAgentChannel.setMessageHandler(nil) | 420 | getFullUserAgentChannel.setMessageHandler(nil) |
| 400 | } | 421 | } |
| 422 | + /// 是否是测试环境 | ||
| 423 | + let isDebugEnvorimentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 424 | + if let api = api { | ||
| 425 | + isDebugEnvorimentChannel.setMessageHandler { _, reply in | ||
| 426 | + do { | ||
| 427 | + let result = try api.isDebugEnvoriment() | ||
| 428 | + reply(wrapResult(result)) | ||
| 429 | + } catch { | ||
| 430 | + reply(wrapError(error)) | ||
| 431 | + } | ||
| 432 | + } | ||
| 433 | + } else { | ||
| 434 | + isDebugEnvorimentChannel.setMessageHandler(nil) | ||
| 435 | + } | ||
| 401 | /// 更新用户信息, 有登录态后调用 | 436 | /// 更新用户信息, 有登录态后调用 |
| 402 | /// jsonString: UserPreferences的序列化string | 437 | /// jsonString: UserPreferences的序列化string |
| 403 | /// baseUrl: 请求地址, https://api.doublefeel.cn | 438 | /// baseUrl: 请求地址, https://api.doublefeel.cn |
| @@ -445,7 +480,7 @@ class PlatformHostApiSetup { | @@ -445,7 +480,7 @@ class PlatformHostApiSetup { | ||
| 445 | } else { | 480 | } else { |
| 446 | refreshVipChannel.setMessageHandler(nil) | 481 | refreshVipChannel.setMessageHandler(nil) |
| 447 | } | 482 | } |
| 448 | - /// 刷新watch app 和 表盘的所有数据: | 483 | + /// 刷新watch app 和 表盘的所有数据: |
| 449 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) | 484 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) |
| 450 | let refreshWatchAppAndWidgetsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 485 | let refreshWatchAppAndWidgetsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 451 | if let api = api { | 486 | if let api = api { |
| @@ -460,6 +495,22 @@ class PlatformHostApiSetup { | @@ -460,6 +495,22 @@ class PlatformHostApiSetup { | ||
| 460 | } else { | 495 | } else { |
| 461 | refreshWatchAppAndWidgetsChannel.setMessageHandler(nil) | 496 | refreshWatchAppAndWidgetsChannel.setMessageHandler(nil) |
| 462 | } | 497 | } |
| 498 | + /// 请求评分弹窗 | ||
| 499 | + let requestAppReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 500 | + if let api = api { | ||
| 501 | + requestAppReviewChannel.setMessageHandler { _, reply in | ||
| 502 | + api.requestAppReview { result in | ||
| 503 | + switch result { | ||
| 504 | + case .success(let res): | ||
| 505 | + reply(wrapResult(res)) | ||
| 506 | + case .failure(let error): | ||
| 507 | + reply(wrapError(error)) | ||
| 508 | + } | ||
| 509 | + } | ||
| 510 | + } | ||
| 511 | + } else { | ||
| 512 | + requestAppReviewChannel.setMessageHandler(nil) | ||
| 513 | + } | ||
| 463 | /// 请求苹果登录 | 514 | /// 请求苹果登录 |
| 464 | let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 515 | let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 465 | if let api = api { | 516 | if let api = api { |
| @@ -532,5 +583,25 @@ class PlatformHostApiSetup { | @@ -532,5 +583,25 @@ class PlatformHostApiSetup { | ||
| 532 | } else { | 583 | } else { |
| 533 | performRestoreChannel.setMessageHandler(nil) | 584 | performRestoreChannel.setMessageHandler(nil) |
| 534 | } | 585 | } |
| 586 | + /// 上传文件到云端 | ||
| 587 | + /// 注意catch flutter error | ||
| 588 | + let uploadFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 589 | + if let api = api { | ||
| 590 | + uploadFileChannel.setMessageHandler { message, reply in | ||
| 591 | + let args = message as! [Any?] | ||
| 592 | + let filePathArg = args[0] as! String | ||
| 593 | + let resourceTypeArg = args[1] as! HResourceType | ||
| 594 | + api.uploadFile(filePath: filePathArg, resourceType: resourceTypeArg) { result in | ||
| 595 | + switch result { | ||
| 596 | + case .success(let res): | ||
| 597 | + reply(wrapResult(res)) | ||
| 598 | + case .failure(let error): | ||
| 599 | + reply(wrapError(error)) | ||
| 600 | + } | ||
| 601 | + } | ||
| 602 | + } | ||
| 603 | + } else { | ||
| 604 | + uploadFileChannel.setMessageHandler(nil) | ||
| 605 | + } | ||
| 535 | } | 606 | } |
| 536 | } | 607 | } |
| @@ -37,6 +37,18 @@ class PriceFormatter{ | @@ -37,6 +37,18 @@ class PriceFormatter{ | ||
| 37 | * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` | 37 | * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` |
| 38 | */ | 38 | */ |
| 39 | final class PlatformHostApiImpl: PlatformHostApi { | 39 | final class PlatformHostApiImpl: PlatformHostApi { |
| 40 | + func isDebugEnvoriment() throws -> Bool { | ||
| 41 | + return true | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + func requestAppReview(completion: @escaping (Result<Bool, any Error>) -> Void) { | ||
| 45 | + | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) { | ||
| 49 | + | ||
| 50 | + } | ||
| 51 | + | ||
| 40 | func logout() throws { | 52 | func logout() throws { |
| 41 | 53 | ||
| 42 | } | 54 | } |
| @@ -192,6 +192,8 @@ protocol WearEngineHostApi { | @@ -192,6 +192,8 @@ protocol WearEngineHostApi { | ||
| 192 | /// Opens the system photo picker and returns a local PNG file path after | 192 | /// Opens the system photo picker and returns a local PNG file path after |
| 193 | /// removing the image background on the host platform. | 193 | /// removing the image background on the host platform. |
| 194 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void) | 194 | func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void) |
| 195 | + /// 是否有已安装的表盘 | ||
| 196 | + func hasInstalledWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 195 | } | 197 | } |
| 196 | 198 | ||
| 197 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | 199 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. |
| @@ -288,5 +290,21 @@ class WearEngineHostApiSetup { | @@ -288,5 +290,21 @@ class WearEngineHostApiSetup { | ||
| 288 | } else { | 290 | } else { |
| 289 | removeBackgroundChannel.setMessageHandler(nil) | 291 | removeBackgroundChannel.setMessageHandler(nil) |
| 290 | } | 292 | } |
| 293 | + /// 是否有已安装的表盘 | ||
| 294 | + let hasInstalledWatchSurfaceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 295 | + if let api = api { | ||
| 296 | + hasInstalledWatchSurfaceChannel.setMessageHandler { _, reply in | ||
| 297 | + api.hasInstalledWatchSurface { result in | ||
| 298 | + switch result { | ||
| 299 | + case .success(let res): | ||
| 300 | + reply(wrapResult(res)) | ||
| 301 | + case .failure(let error): | ||
| 302 | + reply(wrapError(error)) | ||
| 303 | + } | ||
| 304 | + } | ||
| 305 | + } | ||
| 306 | + } else { | ||
| 307 | + hasInstalledWatchSurfaceChannel.setMessageHandler(nil) | ||
| 308 | + } | ||
| 291 | } | 309 | } |
| 292 | } | 310 | } |
| @@ -8,6 +8,10 @@ enum ImageProcessError: Error{ | @@ -8,6 +8,10 @@ enum ImageProcessError: Error{ | ||
| 8 | } | 8 | } |
| 9 | 9 | ||
| 10 | final class WearEngineHostApiImpl: WearEngineHostApi { | 10 | final class WearEngineHostApiImpl: WearEngineHostApi { |
| 11 | + func hasInstalledWatchSurface(completion: @escaping (Result<Bool, any Error>) -> Void) { | ||
| 12 | + | ||
| 13 | + } | ||
| 14 | + | ||
| 11 | private let watchService: WatchConnectivityService | 15 | private let watchService: WatchConnectivityService |
| 12 | 16 | ||
| 13 | init(watchService: WatchConnectivityService = .shared) { | 17 | init(watchService: WatchConnectivityService = .shared) { |
| @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_v | @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_v | ||
| 6 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| 7 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 7 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 8 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | 8 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 9 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 9 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 10 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 10 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | 11 | import 'package:doublefeel_flutter/core/util/size_extensions.dart'; |
| 11 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 12 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| @@ -24,6 +25,7 @@ class MyTab extends GetView<MyController> { | @@ -24,6 +25,7 @@ class MyTab extends GetView<MyController> { | ||
| 24 | @override | 25 | @override |
| 25 | Widget build(BuildContext context) { | 26 | Widget build(BuildContext context) { |
| 26 | final userPrefs = Get.find<UserPreferencesStorage>(); | 27 | final userPrefs = Get.find<UserPreferencesStorage>(); |
| 28 | + final environmentConfig = Get.find<AppEnvironmentConfig>(); | ||
| 27 | 29 | ||
| 28 | return Container( | 30 | return Container( |
| 29 | color: context.colors.backgroundPage, | 31 | color: context.colors.backgroundPage, |
| @@ -74,7 +76,7 @@ class MyTab extends GetView<MyController> { | @@ -74,7 +76,7 @@ class MyTab extends GetView<MyController> { | ||
| 74 | controller.testAppleHealthUpload(); | 76 | controller.testAppleHealthUpload(); |
| 75 | }, | 77 | }, |
| 76 | ), | 78 | ), |
| 77 | - if (kDebugMode) ...[ | 79 | + if (environmentConfig.isDebug) ...[ |
| 78 | const SizedBox(height: 12), | 80 | const SizedBox(height: 12), |
| 79 | _SettingsRow( | 81 | _SettingsRow( |
| 80 | title: 'Route List', | 82 | title: 'Route List', |
| @@ -26,6 +26,7 @@ class LoginController extends GetxController { | @@ -26,6 +26,7 @@ class LoginController extends GetxController { | ||
| 26 | final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>(); | 26 | final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>(); |
| 27 | final UserAccountStorage _userAccount = Get.find<UserAccountStorage>(); | 27 | final UserAccountStorage _userAccount = Get.find<UserAccountStorage>(); |
| 28 | final UserStateService _userStateService = Get.find<UserStateService>(); | 28 | final UserStateService _userStateService = Get.find<UserStateService>(); |
| 29 | + final environmentConfig = Get.find<AppEnvironmentConfig>(); | ||
| 29 | 30 | ||
| 30 | final phoneController = TextEditingController(); | 31 | final phoneController = TextEditingController(); |
| 31 | final codeController = TextEditingController(); | 32 | final codeController = TextEditingController(); |
| @@ -178,7 +178,7 @@ class LoginView extends GetView<LoginController> { | @@ -178,7 +178,7 @@ class LoginView extends GetView<LoginController> { | ||
| 178 | child: const _AgreementText(), | 178 | child: const _AgreementText(), |
| 179 | ), | 179 | ), |
| 180 | 180 | ||
| 181 | - if (kDebugMode) | 181 | + if (controller.environmentConfig.isDebug) |
| 182 | Positioned( | 182 | Positioned( |
| 183 | left: 0, | 183 | left: 0, |
| 184 | right: 0, | 184 | right: 0, |
| 1 | +import 'dart:async'; | ||
| 2 | +import 'dart:io'; | ||
| 3 | + | ||
| 1 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 4 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 2 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; |
| 3 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 6 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 4 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 7 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 8 | +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | ||
| 5 | import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; | 9 | import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; |
| 6 | import 'package:flutter/material.dart'; | 10 | import 'package:flutter/material.dart'; |
| 7 | import 'package:get/get.dart'; | 11 | import 'package:get/get.dart'; |
| 12 | +import 'package:image_cropper/image_cropper.dart'; | ||
| 8 | import 'package:image_picker/image_picker.dart'; | 13 | import 'package:image_picker/image_picker.dart'; |
| 14 | +import 'package:path_provider/path_provider.dart'; | ||
| 9 | 15 | ||
| 10 | import '../models/watch_theme_models.dart'; | 16 | import '../models/watch_theme_models.dart'; |
| 11 | import '../widgets/watch_theme_dialogs.dart'; | 17 | import '../widgets/watch_theme_dialogs.dart'; |
| @@ -14,13 +20,18 @@ class CreateWatchThemeController extends GetxController { | @@ -14,13 +20,18 @@ class CreateWatchThemeController extends GetxController { | ||
| 14 | CreateWatchThemeController(this._themeApi); | 20 | CreateWatchThemeController(this._themeApi); |
| 15 | 21 | ||
| 16 | final ThemeApi _themeApi; | 22 | final ThemeApi _themeApi; |
| 23 | + final PlatformHostApi _platformHostApi = PlatformHostApi(); | ||
| 17 | final WearEngineHostApi _wearEngineHostApi = WearEngineHostApi(); | 24 | final WearEngineHostApi _wearEngineHostApi = WearEngineHostApi(); |
| 25 | + final ImagePicker _imagePicker = ImagePicker(); | ||
| 18 | 26 | ||
| 19 | final customThemeName = ''.obs; | 27 | final customThemeName = ''.obs; |
| 20 | final agreedToSubmission = true.obs; | 28 | final agreedToSubmission = true.obs; |
| 21 | final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs; | 29 | final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs; |
| 22 | final customImagePaths = RxList<String?>.filled(4, null); | 30 | final customImagePaths = RxList<String?>.filled(4, null); |
| 31 | + final isRemovingBackground = RxList<bool>.filled(4, false); | ||
| 23 | final isSaving = false.obs; | 32 | final isSaving = false.obs; |
| 33 | + final List<int> _imageTaskVersions = List<int>.filled(4, 0); | ||
| 34 | + bool _isClosed = false; | ||
| 24 | late final TextEditingController themeNameController; | 35 | late final TextEditingController themeNameController; |
| 25 | 36 | ||
| 26 | @override | 37 | @override |
| @@ -34,6 +45,7 @@ class CreateWatchThemeController extends GetxController { | @@ -34,6 +45,7 @@ class CreateWatchThemeController extends GetxController { | ||
| 34 | 45 | ||
| 35 | @override | 46 | @override |
| 36 | void onClose() { | 47 | void onClose() { |
| 48 | + _isClosed = true; | ||
| 37 | themeNameController.dispose(); | 49 | themeNameController.dispose(); |
| 38 | super.onClose(); | 50 | super.onClose(); |
| 39 | } | 51 | } |
| @@ -42,15 +54,136 @@ class CreateWatchThemeController extends GetxController { | @@ -42,15 +54,136 @@ class CreateWatchThemeController extends GetxController { | ||
| 42 | !isSaving.value && | 54 | !isSaving.value && |
| 43 | customThemeName.value.isNotEmpty && | 55 | customThemeName.value.isNotEmpty && |
| 44 | agreedToSubmission.value && | 56 | agreedToSubmission.value && |
| 57 | + !isRemovingBackground.any((isProcessing) => isProcessing) && | ||
| 45 | customImagePaths.every((path) => path != null && path.isNotEmpty); | 58 | customImagePaths.every((path) => path != null && path.isNotEmpty); |
| 46 | 59 | ||
| 47 | Future<void> pickCustomImage(int index) async { | 60 | Future<void> pickCustomImage(int index) async { |
| 48 | - final picker = ImagePicker(); | ||
| 49 | - final XFile? image = await picker.pickImage(source: ImageSource.gallery); | ||
| 50 | - if (image != null) { | ||
| 51 | - customImagePaths[index] = image.path; | ||
| 52 | - } else { | ||
| 53 | - AppToast.show('图片选择失败'); | 61 | + if (index < 0 || index >= customImagePaths.length) { |
| 62 | + return; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + try { | ||
| 66 | + final image = await _imagePicker.pickImage(source: ImageSource.gallery); | ||
| 67 | + if (image == null) { | ||
| 68 | + return; | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + final croppedImage = await ImageCropper().cropImage( | ||
| 72 | + sourcePath: image.path, | ||
| 73 | + aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), | ||
| 74 | + maxWidth: 280, | ||
| 75 | + maxHeight: 280, | ||
| 76 | + compressFormat: ImageCompressFormat.png, | ||
| 77 | + compressQuality: 100, | ||
| 78 | + uiSettings: [ | ||
| 79 | + AndroidUiSettings( | ||
| 80 | + toolbarTitle: '裁剪表盘图片', | ||
| 81 | + lockAspectRatio: true, | ||
| 82 | + ), | ||
| 83 | + IOSUiSettings( | ||
| 84 | + title: '裁剪表盘图片', | ||
| 85 | + aspectRatioLockEnabled: true, | ||
| 86 | + resetAspectRatioEnabled: false, | ||
| 87 | + ), | ||
| 88 | + ], | ||
| 89 | + ); | ||
| 90 | + if (croppedImage == null) { | ||
| 91 | + return; | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + final savedPath = await _saveCroppedImage(croppedImage.path, index); | ||
| 95 | + await _deleteReplacedImage(customImagePaths[index], savedPath); | ||
| 96 | + customImagePaths[index] = savedPath; | ||
| 97 | + final taskVersion = ++_imageTaskVersions[index]; | ||
| 98 | + isRemovingBackground[index] = true; | ||
| 99 | + | ||
| 100 | + // Pigeon 方法是异步接口,iOS 端的 Vision 请求在 Task.detached 中执行。 | ||
| 101 | + // 此处不等待结果,让裁切图先显示;处理完成后再原位替换。 | ||
| 102 | + unawaited(_removeBackgroundAndReplace(index, savedPath, taskVersion)); | ||
| 103 | + } catch (_) { | ||
| 104 | + AppToast.show('图片处理失败,请重试'); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + Future<void> _removeBackgroundAndReplace( | ||
| 109 | + int index, | ||
| 110 | + String croppedImagePath, | ||
| 111 | + int taskVersion, | ||
| 112 | + ) async { | ||
| 113 | + String? savedProcessedPath; | ||
| 114 | + try { | ||
| 115 | + final processedPath = | ||
| 116 | + await _wearEngineHostApi.removeBackground(croppedImagePath); | ||
| 117 | + if (processedPath == null || processedPath.isEmpty) { | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + savedProcessedPath = await _saveProcessedImage(processedPath, index); | ||
| 122 | + if (!_isCurrentImageTask(index, croppedImagePath, taskVersion)) { | ||
| 123 | + await _deleteReplacedImage(savedProcessedPath, ''); | ||
| 124 | + return; | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + customImagePaths[index] = savedProcessedPath; | ||
| 128 | + await _deleteReplacedImage(croppedImagePath, savedProcessedPath); | ||
| 129 | + } catch (error, stackTrace) { | ||
| 130 | + debugPrint('Watch theme background removal failed: $error'); | ||
| 131 | + debugPrintStack(stackTrace: stackTrace); | ||
| 132 | + } finally { | ||
| 133 | + if (!_isClosed && _imageTaskVersions[index] == taskVersion) { | ||
| 134 | + isRemovingBackground[index] = false; | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + bool _isCurrentImageTask( | ||
| 140 | + int index, | ||
| 141 | + String croppedImagePath, | ||
| 142 | + int taskVersion, | ||
| 143 | + ) { | ||
| 144 | + return !_isClosed && | ||
| 145 | + _imageTaskVersions[index] == taskVersion && | ||
| 146 | + customImagePaths[index] == croppedImagePath; | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + Future<String> _saveCroppedImage(String sourcePath, int index) async { | ||
| 150 | + return _saveThemeImage(sourcePath, index, 'cropped'); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + Future<String> _saveProcessedImage(String sourcePath, int index) async { | ||
| 154 | + return _saveThemeImage(sourcePath, index, 'processed'); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + Future<String> _saveThemeImage( | ||
| 158 | + String sourcePath, | ||
| 159 | + int index, | ||
| 160 | + String stage, | ||
| 161 | + ) async { | ||
| 162 | + final documentsDirectory = await getApplicationDocumentsDirectory(); | ||
| 163 | + final themeImageDirectory = | ||
| 164 | + Directory('${documentsDirectory.path}/watch_theme'); | ||
| 165 | + await themeImageDirectory.create(recursive: true); | ||
| 166 | + | ||
| 167 | + final fileName = 'watch_theme_${index}_${stage}_' | ||
| 168 | + '${DateTime.now().microsecondsSinceEpoch}.png'; | ||
| 169 | + final savedFile = await File(sourcePath).copy( | ||
| 170 | + '${themeImageDirectory.path}/$fileName', | ||
| 171 | + ); | ||
| 172 | + return savedFile.path; | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + Future<void> _deleteReplacedImage( | ||
| 176 | + String? previousPath, | ||
| 177 | + String replacementPath, | ||
| 178 | + ) async { | ||
| 179 | + if (previousPath == null || previousPath == replacementPath) { | ||
| 180 | + return; | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + final previousFile = File(previousPath); | ||
| 184 | + if (previousFile.parent.path.endsWith('/watch_theme') && | ||
| 185 | + await previousFile.exists()) { | ||
| 186 | + await previousFile.delete(); | ||
| 54 | } | 187 | } |
| 55 | } | 188 | } |
| 56 | 189 | ||
| @@ -95,73 +228,61 @@ class CreateWatchThemeController extends GetxController { | @@ -95,73 +228,61 @@ class CreateWatchThemeController extends GetxController { | ||
| 95 | } | 228 | } |
| 96 | 229 | ||
| 97 | isSaving.value = true; | 230 | isSaving.value = true; |
| 98 | - | ||
| 99 | - var imagePath1 = customImagePaths[0]; | ||
| 100 | - var imagePath2 = customImagePaths[1]; | ||
| 101 | - var imagePath3 = customImagePaths[2]; | ||
| 102 | - var imagePath4 = customImagePaths[3]; | ||
| 103 | - | ||
| 104 | - if (imagePath1 != null) { | ||
| 105 | - final path = await _wearEngineHostApi.removeBackground(imagePath1); | ||
| 106 | - if (path != null) { | ||
| 107 | - imagePath1 = path; | ||
| 108 | - } | ||
| 109 | - } | ||
| 110 | - if (imagePath2 != null) { | ||
| 111 | - final path = await _wearEngineHostApi.removeBackground(imagePath2); | ||
| 112 | - if (path != null) { | ||
| 113 | - imagePath2 = path; | ||
| 114 | - } | ||
| 115 | - } | ||
| 116 | - if (imagePath3 != null) { | ||
| 117 | - final path = await _wearEngineHostApi.removeBackground(imagePath3); | ||
| 118 | - if (path != null) { | ||
| 119 | - imagePath3 = path; | 231 | + try { |
| 232 | + final uploadedImageUrls = <String>[]; | ||
| 233 | + for (final imagePath in customImagePaths) { | ||
| 234 | + final imageUrl = await _platformHostApi.uploadFile( | ||
| 235 | + imagePath!, | ||
| 236 | + HResourceType.image, | ||
| 237 | + ); | ||
| 238 | + if (imageUrl == null || imageUrl.isEmpty) { | ||
| 239 | + AppToast.show('图片上传失败,请重试'); | ||
| 240 | + return; | ||
| 241 | + } | ||
| 242 | + uploadedImageUrls.add(imageUrl); | ||
| 120 | } | 243 | } |
| 121 | - } | ||
| 122 | - if (imagePath4 != null) { | ||
| 123 | - final path = await _wearEngineHostApi.removeBackground(imagePath4); | ||
| 124 | - if (path != null) { | ||
| 125 | - imagePath4 = path; | 244 | + |
| 245 | + final result = await _themeApi.createTheme( | ||
| 246 | + themeName: customThemeName.value, | ||
| 247 | + energeticDescription: customStatusNames[0], | ||
| 248 | + energeticImage: uploadedImageUrls[0], | ||
| 249 | + normalDescription: customStatusNames[1], | ||
| 250 | + normalImage: uploadedImageUrls[1], | ||
| 251 | + slightStressfulDescription: customStatusNames[2], | ||
| 252 | + slightStressfulImage: uploadedImageUrls[2], | ||
| 253 | + stressfulDescription: customStatusNames[3], | ||
| 254 | + stressfulImage: uploadedImageUrls[3], | ||
| 255 | + ); | ||
| 256 | + if (result is AppFailure<void>) { | ||
| 257 | + AppToast.show(result.error.displayMessage); | ||
| 258 | + return; | ||
| 126 | } | 259 | } |
| 127 | - } | ||
| 128 | 260 | ||
| 129 | - //TODO: - 上传图片 | ||
| 130 | - | ||
| 131 | - // final result = await _themeApi.createTheme( | ||
| 132 | - // themeName: customThemeName.value, | ||
| 133 | - // energeticDescription: customStatusNames[0], | ||
| 134 | - // energeticImage: customImagePaths[0] ?? '', | ||
| 135 | - // normalDescription: customStatusNames[1], | ||
| 136 | - // normalImage: customImagePaths[1] ?? '', | ||
| 137 | - // slightStressfulDescription: customStatusNames[2], | ||
| 138 | - // slightStressfulImage: customImagePaths[2] ?? '', | ||
| 139 | - // stressfulDescription: customStatusNames[3], | ||
| 140 | - // stressfulImage: customImagePaths[3] ?? '', | ||
| 141 | - // ); | ||
| 142 | - isSaving.value = false; | ||
| 143 | - | ||
| 144 | - //Test: - 测试抠图结果 | ||
| 145 | - customImagePaths.value = [imagePath1, imagePath2, imagePath3, imagePath4]; | ||
| 146 | - customImagePaths.refresh(); | ||
| 147 | - | ||
| 148 | - // final createdTheme = await _loadCreatedTheme(); | ||
| 149 | - // Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: { | ||
| 150 | - // 'theme': createdTheme ?? _buildThemeItem(), | ||
| 151 | - // }); | 261 | + final createdTheme = await _loadCreatedTheme(); |
| 262 | + await Get.toNamed( | ||
| 263 | + Routes.WATCH_THEME_CUSTOM_PREVIEW, | ||
| 264 | + arguments: { | ||
| 265 | + 'theme': createdTheme ?? _buildThemeItem(uploadedImageUrls), | ||
| 266 | + }, | ||
| 267 | + ); | ||
| 268 | + } catch (_) { | ||
| 269 | + AppToast.show('创建表盘失败,请重试'); | ||
| 270 | + } finally { | ||
| 271 | + isSaving.value = false; | ||
| 272 | + } | ||
| 152 | } | 273 | } |
| 153 | 274 | ||
| 154 | - WatchThemeItem _buildThemeItem() { | 275 | + WatchThemeItem _buildThemeItem(List<String> imageUrls) { |
| 155 | return WatchThemeItem( | 276 | return WatchThemeItem( |
| 156 | themeName: customThemeName.value, | 277 | themeName: customThemeName.value, |
| 157 | energeticDescription: customStatusNames[0], | 278 | energeticDescription: customStatusNames[0], |
| 158 | - energeticImage: customImagePaths[0], | 279 | + energeticImage: imageUrls[0], |
| 159 | normalDescription: customStatusNames[1], | 280 | normalDescription: customStatusNames[1], |
| 160 | - normalImage: customImagePaths[1], | 281 | + normalImage: imageUrls[1], |
| 161 | slightStressfulDescription: customStatusNames[2], | 282 | slightStressfulDescription: customStatusNames[2], |
| 162 | - slightStressfulImage: customImagePaths[2], | 283 | + slightStressfulImage: imageUrls[2], |
| 163 | stressfulDescription: customStatusNames[3], | 284 | stressfulDescription: customStatusNames[3], |
| 164 | - stressfulImage: customImagePaths[3], | 285 | + stressfulImage: imageUrls[3], |
| 165 | ); | 286 | ); |
| 166 | } | 287 | } |
| 167 | 288 |
| @@ -71,6 +71,10 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -71,6 +71,10 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 71 | } | 71 | } |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | + _changeFriend() { | ||
| 75 | + //TODO: - bottomSheet 的方式展示出 SelectFriendView | ||
| 76 | + } | ||
| 77 | + | ||
| 74 | Future<void> addWatchFace() async { | 78 | Future<void> addWatchFace() async { |
| 75 | WearDeviceInfo? device; | 79 | WearDeviceInfo? device; |
| 76 | try { | 80 | try { |
| @@ -89,7 +93,7 @@ class CustomWatchThemePreviewController extends GetxController { | @@ -89,7 +93,7 @@ class CustomWatchThemePreviewController extends GetxController { | ||
| 89 | 93 | ||
| 90 | await Get.dialog<void>( | 94 | await Get.dialog<void>( |
| 91 | WatchThemeSyncDialog( | 95 | WatchThemeSyncDialog( |
| 92 | - faceAsset: R.assetsImagesWatchThemeCustomFacePreview, | 96 | + themeImageUrl: themeItem.energeticImage, |
| 93 | onSync: _applyAndSyncWatchFace, | 97 | onSync: _applyAndSyncWatchFace, |
| 94 | ), | 98 | ), |
| 95 | barrierDismissible: false, | 99 | barrierDismissible: false, |
| @@ -59,7 +59,7 @@ class WatchThemePreviewController extends GetxController { | @@ -59,7 +59,7 @@ class WatchThemePreviewController extends GetxController { | ||
| 59 | 59 | ||
| 60 | await Get.dialog<void>( | 60 | await Get.dialog<void>( |
| 61 | WatchThemeSyncDialog( | 61 | WatchThemeSyncDialog( |
| 62 | - faceAsset: R.assetsImagesWatchThemeFaceDefault, | 62 | + themeImageUrl: themeItem.energeticImage, |
| 63 | onSync: _applyAndSyncWatchFace, | 63 | onSync: _applyAndSyncWatchFace, |
| 64 | ), | 64 | ), |
| 65 | barrierDismissible: false, | 65 | barrierDismissible: false, |
| @@ -67,6 +67,10 @@ class WatchThemePreviewController extends GetxController { | @@ -67,6 +67,10 @@ class WatchThemePreviewController extends GetxController { | ||
| 67 | ); | 67 | ); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | + _changeFriend() { | ||
| 71 | + //TODO: - bottomSheet 的方式展示出 SelectFriendView | ||
| 72 | + } | ||
| 73 | + | ||
| 70 | Future<bool> _applyAndSyncWatchFace() async { | 74 | Future<bool> _applyAndSyncWatchFace() async { |
| 71 | final themeId = themeItem.id; | 75 | final themeId = themeItem.id; |
| 72 | if (themeId == null) { | 76 | if (themeId == null) { |
| 1 | import 'dart:io'; | 1 | import 'dart:io'; |
| 2 | 2 | ||
| 3 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 4 | -import 'package:doublefeel_flutter/r.dart'; | ||
| 5 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 6 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 7 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| @@ -64,10 +62,13 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> { | @@ -64,10 +62,13 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> { | ||
| 64 | child: Column( | 62 | child: Column( |
| 65 | children: [ | 63 | children: [ |
| 66 | SizedBox(height: 12), | 64 | SizedBox(height: 12), |
| 67 | - WatchFacePreview( | ||
| 68 | - faceAsset: | ||
| 69 | - R.assetsImagesWatchThemeCustomFaceCreate, | ||
| 70 | - ), | 65 | + Obx(() { |
| 66 | + return WatchFacePreview( | ||
| 67 | + type: WatchFacePreviewType.singleMedium, | ||
| 68 | + themeImageUrl: | ||
| 69 | + controller.customImagePaths.first, | ||
| 70 | + ); | ||
| 71 | + }), | ||
| 71 | SizedBox(height: 20), | 72 | SizedBox(height: 20), |
| 72 | _EditorCard(controller: controller), | 73 | _EditorCard(controller: controller), |
| 73 | SizedBox(height: 26), | 74 | SizedBox(height: 26), |
| 1 | -import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | ||
| 2 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | 1 | +import 'package:doublefeel_flutter/app/modules/watch_theme/widgets/dial_preview_card.dart'; |
| 3 | import 'package:doublefeel_flutter/r.dart'; | 2 | import 'package:doublefeel_flutter/r.dart'; |
| 4 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 5 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 6 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 7 | 6 | ||
| 8 | import '../controllers/custom_watch_theme_preview_controller.dart'; | 7 | import '../controllers/custom_watch_theme_preview_controller.dart'; |
| 8 | +import '../widgets/status_preview_card.dart'; | ||
| 9 | import '../widgets/watch_face_preview.dart'; | 9 | import '../widgets/watch_face_preview.dart'; |
| 10 | import '../widgets/watch_theme_bottom_actions.dart'; | 10 | import '../widgets/watch_theme_bottom_actions.dart'; |
| 11 | import '../widgets/watch_theme_colors.dart'; | 11 | import '../widgets/watch_theme_colors.dart'; |
| @@ -62,7 +62,7 @@ class CustomWatchThemePreviewView | @@ -62,7 +62,7 @@ class CustomWatchThemePreviewView | ||
| 62 | '删除', | 62 | '删除', |
| 63 | style: TextStyle( | 63 | style: TextStyle( |
| 64 | color: const Color(0xFFFC4447), | 64 | color: const Color(0xFFFC4447), |
| 65 | - fontSize: 14.dp, | 65 | + fontSize: 14, |
| 66 | fontWeight: FontWeight.w500, | 66 | fontWeight: FontWeight.w500, |
| 67 | ), | 67 | ), |
| 68 | ), | 68 | ), |
| @@ -71,18 +71,20 @@ class CustomWatchThemePreviewView | @@ -71,18 +71,20 @@ class CustomWatchThemePreviewView | ||
| 71 | Expanded( | 71 | Expanded( |
| 72 | child: SingleChildScrollView( | 72 | child: SingleChildScrollView( |
| 73 | physics: const ClampingScrollPhysics(), | 73 | physics: const ClampingScrollPhysics(), |
| 74 | - padding: EdgeInsets.only(bottom: 96.dp), | 74 | + padding: const EdgeInsets.only(bottom: 96), |
| 75 | child: Column( | 75 | child: Column( |
| 76 | children: [ | 76 | children: [ |
| 77 | WatchThemeHeader( | 77 | WatchThemeHeader( |
| 78 | title: controller.themeItem.title, | 78 | title: controller.themeItem.title, |
| 79 | - faceAsset: | ||
| 80 | - R.assetsImagesWatchThemeCustomFacePreview, | 79 | + themeImageUrl: controller.themeItem.energeticImage, |
| 81 | ), | 80 | ), |
| 82 | - SizedBox(height: 26.dp), | ||
| 83 | - _CustomStatusPreviewCard(controller: controller), | ||
| 84 | - SizedBox(height: 12.dp), | ||
| 85 | - const _CustomDialPreviewCard(), | 81 | + const SizedBox(height: 26), |
| 82 | + StatusPreviewCard(themeItem: controller.themeItem), | ||
| 83 | + const SizedBox(height: 12), | ||
| 84 | + DialPreviewCard( | ||
| 85 | + showFriend: true, | ||
| 86 | + themeImageUrl: controller.themeItem.energeticImage, | ||
| 87 | + ) | ||
| 86 | ], | 88 | ], |
| 87 | ), | 89 | ), |
| 88 | ), | 90 | ), |
| @@ -102,92 +104,3 @@ class CustomWatchThemePreviewView | @@ -102,92 +104,3 @@ class CustomWatchThemePreviewView | ||
| 102 | ); | 104 | ); |
| 103 | } | 105 | } |
| 104 | } | 106 | } |
| 105 | - | ||
| 106 | -class _CustomStatusPreviewCard extends StatelessWidget { | ||
| 107 | - const _CustomStatusPreviewCard({required this.controller}); | ||
| 108 | - | ||
| 109 | - final CustomWatchThemePreviewController controller; | ||
| 110 | - | ||
| 111 | - static final _assets = [ | ||
| 112 | - R.assetsImagesWatchThemeCustomStatusExcellent, | ||
| 113 | - R.assetsImagesWatchThemeCustomStatusNormal, | ||
| 114 | - R.assetsImagesWatchThemeCustomStatusStress, | ||
| 115 | - R.assetsImagesWatchThemeCustomStatusOverload, | ||
| 116 | - ]; | ||
| 117 | - | ||
| 118 | - | ||
| 119 | - @override | ||
| 120 | - Widget build(BuildContext context) { | ||
| 121 | - final infoList = controller.themeItem.infoList; | ||
| 122 | - return WatchThemeSectionCard( | ||
| 123 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp), | ||
| 124 | - child: Column( | ||
| 125 | - crossAxisAlignment: CrossAxisAlignment.start, | ||
| 126 | - children: [ | ||
| 127 | - const WatchThemeSectionTitle('状态预览'), | ||
| 128 | - SizedBox(height: 24.dp), | ||
| 129 | - Row( | ||
| 130 | - mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| 131 | - children: [ | ||
| 132 | - for (var i = 0; i < 4; i++) | ||
| 133 | - Column( | ||
| 134 | - children: [ | ||
| 135 | - ClipRRect( | ||
| 136 | - borderRadius: BorderRadius.circular(16.dp), | ||
| 137 | - child: Image.asset( | ||
| 138 | - _assets[i], | ||
| 139 | - width: 60.dp, | ||
| 140 | - height: 60.dp, | ||
| 141 | - fit: BoxFit.cover, | ||
| 142 | - ), | ||
| 143 | - ), | ||
| 144 | - SizedBox(height: 4.dp), | ||
| 145 | - Text( | ||
| 146 | - i < infoList.length ? infoList[i].title : '', | ||
| 147 | - style: TextStyle( | ||
| 148 | - color: defaultThemeTemplates[i].color, | ||
| 149 | - fontSize: 12.dp, | ||
| 150 | - ), | ||
| 151 | - ), | ||
| 152 | - ], | ||
| 153 | - ), | ||
| 154 | - ], | ||
| 155 | - ), | ||
| 156 | - ], | ||
| 157 | - ), | ||
| 158 | - ); | ||
| 159 | - } | ||
| 160 | -} | ||
| 161 | - | ||
| 162 | -class _CustomDialPreviewCard extends StatelessWidget { | ||
| 163 | - const _CustomDialPreviewCard(); | ||
| 164 | - | ||
| 165 | - @override | ||
| 166 | - Widget build(BuildContext context) { | ||
| 167 | - return WatchThemeSectionCard( | ||
| 168 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp), | ||
| 169 | - child: Column( | ||
| 170 | - crossAxisAlignment: CrossAxisAlignment.start, | ||
| 171 | - children: [ | ||
| 172 | - const WatchThemeSectionTitle('表盘预览'), | ||
| 173 | - SizedBox(height: 16.dp), | ||
| 174 | - Row( | ||
| 175 | - children: [ | ||
| 176 | - WatchFacePreview( | ||
| 177 | - width: 114, | ||
| 178 | - height: 136, | ||
| 179 | - faceAsset: R.assetsImagesWatchThemeCustomPreviewAlt, | ||
| 180 | - ), | ||
| 181 | - SizedBox(width: 18.dp), | ||
| 182 | - WatchFacePreview( | ||
| 183 | - width: 114, | ||
| 184 | - height: 136, | ||
| 185 | - faceAsset: R.assetsImagesWatchThemeCustomFacePreview, | ||
| 186 | - ), | ||
| 187 | - ], | ||
| 188 | - ), | ||
| 189 | - ], | ||
| 190 | - ), | ||
| 191 | - ); | ||
| 192 | - } | ||
| 193 | -} |
| @@ -55,7 +55,7 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { | @@ -55,7 +55,7 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { | ||
| 55 | ), | 55 | ), |
| 56 | Expanded( | 56 | Expanded( |
| 57 | child: SingleChildScrollView( | 57 | child: SingleChildScrollView( |
| 58 | - physics: const ClampingScrollPhysics(), | 58 | + physics: const AlwaysScrollableScrollPhysics(), |
| 59 | padding: const EdgeInsets.only(bottom: 96), | 59 | padding: const EdgeInsets.only(bottom: 96), |
| 60 | child: Column( | 60 | child: Column( |
| 61 | children: [ | 61 | children: [ |
| @@ -63,7 +63,9 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { | @@ -63,7 +63,9 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> { | ||
| 63 | const SizedBox(height: 26), | 63 | const SizedBox(height: 26), |
| 64 | StatusPreviewCard(themeItem: controller.themeItem), | 64 | StatusPreviewCard(themeItem: controller.themeItem), |
| 65 | const SizedBox(height: 12), | 65 | const SizedBox(height: 12), |
| 66 | - const DialPreviewCard(), | 66 | + DialPreviewCard( |
| 67 | + showFriend: true, | ||
| 68 | + ), | ||
| 67 | ], | 69 | ], |
| 68 | ), | 70 | ), |
| 69 | ), | 71 | ), |
| @@ -55,7 +55,7 @@ class WatchThemeView extends GetView<WatchThemeController> { | @@ -55,7 +55,7 @@ class WatchThemeView extends GetView<WatchThemeController> { | ||
| 55 | Expanded( | 55 | Expanded( |
| 56 | child: Obx( | 56 | child: Obx( |
| 57 | () => SingleChildScrollView( | 57 | () => SingleChildScrollView( |
| 58 | - physics: const BouncingScrollPhysics(), | 58 | + physics: const AlwaysScrollableScrollPhysics(), |
| 59 | padding: const EdgeInsets.only(bottom: 24), | 59 | padding: const EdgeInsets.only(bottom: 24), |
| 60 | child: Column( | 60 | child: Column( |
| 61 | children: [ | 61 | children: [ |
| 1 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 2 | -import 'package:doublefeel_flutter/r.dart'; | 1 | + |
| 3 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 4 | 3 | ||
| 5 | import 'watch_face_preview.dart'; | 4 | import 'watch_face_preview.dart'; |
| 6 | import 'watch_theme_section_card.dart'; | 5 | import 'watch_theme_section_card.dart'; |
| 7 | 6 | ||
| 8 | class DialPreviewCard extends StatelessWidget { | 7 | class DialPreviewCard extends StatelessWidget { |
| 9 | - const DialPreviewCard({super.key}); | 8 | + bool showFriend; |
| 9 | + final String? themeImageUrl; | ||
| 10 | + final String? themeImageUrl2; | ||
| 11 | + DialPreviewCard( | ||
| 12 | + {super.key, | ||
| 13 | + required this.showFriend, | ||
| 14 | + this.themeImageUrl, | ||
| 15 | + this.themeImageUrl2}); | ||
| 10 | 16 | ||
| 11 | @override | 17 | @override |
| 12 | Widget build(BuildContext context) { | 18 | Widget build(BuildContext context) { |
| 13 | return WatchThemeSectionCard( | 19 | return WatchThemeSectionCard( |
| 14 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp), | 20 | + padding: EdgeInsets.fromLTRB(20, 20, 0, 27), |
| 15 | child: Column( | 21 | child: Column( |
| 16 | crossAxisAlignment: CrossAxisAlignment.start, | 22 | crossAxisAlignment: CrossAxisAlignment.start, |
| 17 | children: [ | 23 | children: [ |
| 18 | const WatchThemeSectionTitle('表盘预览'), | 24 | const WatchThemeSectionTitle('表盘预览'), |
| 19 | - SizedBox(height: 16.dp), | 25 | + SizedBox(height: 16), |
| 20 | SizedBox( | 26 | SizedBox( |
| 21 | - height: 136.dp, | 27 | + height: 136, |
| 22 | child: ListView.separated( | 28 | child: ListView.separated( |
| 23 | padding: EdgeInsets.zero, | 29 | padding: EdgeInsets.zero, |
| 24 | scrollDirection: Axis.horizontal, | 30 | scrollDirection: Axis.horizontal, |
| 25 | - physics: const ClampingScrollPhysics(), | 31 | + physics: const AlwaysScrollableScrollPhysics(), |
| 26 | itemBuilder: (context, index) { | 32 | itemBuilder: (context, index) { |
| 33 | + if (index == 0) { | ||
| 34 | + return WatchFacePreview( | ||
| 35 | + type: WatchFacePreviewType.surface, | ||
| 36 | + themeImageUrl: themeImageUrl, | ||
| 37 | + ); | ||
| 38 | + } else if (showFriend && index == 1) { | ||
| 39 | + return WatchFacePreview( | ||
| 40 | + type: WatchFacePreviewType.coupleSmall, | ||
| 41 | + themeImageUrl: themeImageUrl, | ||
| 42 | + themeImageUrl2: themeImageUrl2, | ||
| 43 | + ); | ||
| 44 | + } | ||
| 27 | return WatchFacePreview( | 45 | return WatchFacePreview( |
| 28 | - width: 114, | ||
| 29 | - height: 136, | ||
| 30 | - faceAsset: index == 0 | ||
| 31 | - ? R.assetsImagesWatchThemeFacePreviewAlt | ||
| 32 | - : R.assetsImagesWatchThemeFaceDefault, | 46 | + type: WatchFacePreviewType.singleSmall, |
| 47 | + themeImageUrl: themeImageUrl, | ||
| 33 | ); | 48 | ); |
| 34 | }, | 49 | }, |
| 35 | - separatorBuilder: (context, index) => SizedBox(width: 18.dp), | ||
| 36 | - itemCount: 2, | 50 | + separatorBuilder: (context, index) => SizedBox(width: 12), |
| 51 | + itemCount: showFriend ? 3 : 2, | ||
| 37 | ), | 52 | ), |
| 38 | ), | 53 | ), |
| 39 | ], | 54 | ], |
| 1 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 1 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| 2 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 3 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 4 | 3 | ||
| 5 | import 'watch_theme_character_avatar.dart'; | 4 | import 'watch_theme_character_avatar.dart'; |
| @@ -12,12 +11,12 @@ class StatusPreviewCard extends StatelessWidget { | @@ -12,12 +11,12 @@ class StatusPreviewCard extends StatelessWidget { | ||
| 12 | @override | 11 | @override |
| 13 | Widget build(BuildContext context) { | 12 | Widget build(BuildContext context) { |
| 14 | return WatchThemeSectionCard( | 13 | return WatchThemeSectionCard( |
| 15 | - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp), | 14 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 17), |
| 16 | child: Column( | 15 | child: Column( |
| 17 | crossAxisAlignment: CrossAxisAlignment.start, | 16 | crossAxisAlignment: CrossAxisAlignment.start, |
| 18 | children: [ | 17 | children: [ |
| 19 | const WatchThemeSectionTitle('状态预览'), | 18 | const WatchThemeSectionTitle('状态预览'), |
| 20 | - SizedBox(height: 24.dp), | 19 | + const SizedBox(height: 24), |
| 21 | Row( | 20 | Row( |
| 22 | mainAxisAlignment: MainAxisAlignment.spaceBetween, | 21 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 23 | children: _previewItemView(), | 22 | children: _previewItemView(), |
| @@ -38,12 +37,12 @@ class StatusPreviewCard extends StatelessWidget { | @@ -38,12 +37,12 @@ class StatusPreviewCard extends StatelessWidget { | ||
| 38 | assetPath: infoList[i].assetPath, | 37 | assetPath: infoList[i].assetPath, |
| 39 | imageUrl: infoList[i].imgUrl, | 38 | imageUrl: infoList[i].imgUrl, |
| 40 | ), | 39 | ), |
| 41 | - SizedBox(height: 4.dp), | 40 | + const SizedBox(height: 4), |
| 42 | Text( | 41 | Text( |
| 43 | i < infoList.length ? infoList[i].title : '', | 42 | i < infoList.length ? infoList[i].title : '', |
| 44 | style: TextStyle( | 43 | style: TextStyle( |
| 45 | color: defaultThemeTemplates[i].color, | 44 | color: defaultThemeTemplates[i].color, |
| 46 | - fontSize: 12.dp, | 45 | + fontSize: 12, |
| 47 | fontWeight: FontWeight.w400, | 46 | fontWeight: FontWeight.w400, |
| 48 | ), | 47 | ), |
| 49 | ), | 48 | ), |
| 1 | import 'dart:io'; | 1 | import 'dart:io'; |
| 2 | 2 | ||
| 3 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 4 | -import 'package:doublefeel_flutter/r.dart'; | 3 | +import 'package:cached_network_image/cached_network_image.dart'; |
| 5 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 5 | +import 'package:get/get.dart'; | ||
| 6 | 6 | ||
| 7 | -import 'watch_theme_colors.dart'; | 7 | +enum WatchFacePreviewType { |
| 8 | + surface, | ||
| 9 | + singleSmall, | ||
| 10 | + singleMedium, | ||
| 11 | + coupleSmall, | ||
| 12 | +} | ||
| 8 | 13 | ||
| 9 | class WatchFacePreview extends StatelessWidget { | 14 | class WatchFacePreview extends StatelessWidget { |
| 10 | const WatchFacePreview({ | 15 | const WatchFacePreview({ |
| 11 | super.key, | 16 | super.key, |
| 12 | - this.width = 134, | ||
| 13 | - this.height = 160, | ||
| 14 | - this.faceAsset, | ||
| 15 | - this.faceFilePath, | 17 | + this.type = WatchFacePreviewType.singleMedium, |
| 18 | + this.themeImageUrl, | ||
| 19 | + this.themeImageUrl2, | ||
| 16 | }); | 20 | }); |
| 17 | 21 | ||
| 18 | - final double width; | ||
| 19 | - final double height; | ||
| 20 | - final String? faceAsset; | ||
| 21 | - final String? faceFilePath; | 22 | + final String? themeImageUrl; |
| 23 | + final String? themeImageUrl2; | ||
| 24 | + final WatchFacePreviewType type; | ||
| 25 | + | ||
| 26 | + String get _backgroundAsset { | ||
| 27 | + if (type == WatchFacePreviewType.surface) { | ||
| 28 | + return 'assets/images/watch_theme/watch_theme_preview_surface.png'; | ||
| 29 | + } | ||
| 30 | + if (type == WatchFacePreviewType.coupleSmall) { | ||
| 31 | + return 'assets/images/watch_theme/watch_theme_preview_couple.png'; | ||
| 32 | + } | ||
| 33 | + return 'assets/images/watch_theme/watch_theme_preview_single.png'; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + Size get _previewSize { | ||
| 37 | + if (type == WatchFacePreviewType.singleMedium) { | ||
| 38 | + return Size(134, 160); | ||
| 39 | + } | ||
| 40 | + return Size(120, 136); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + Size get _themeSize { | ||
| 44 | + if (type == WatchFacePreviewType.singleMedium) { | ||
| 45 | + return Size(78, 78); | ||
| 46 | + } | ||
| 47 | + if (type == WatchFacePreviewType.singleSmall) { | ||
| 48 | + return Size(55, 55); | ||
| 49 | + } | ||
| 50 | + if (type == WatchFacePreviewType.surface) { | ||
| 51 | + return Size(44, 44); | ||
| 52 | + } | ||
| 53 | + return Size(55, 55); | ||
| 54 | + } | ||
| 22 | 55 | ||
| 23 | @override | 56 | @override |
| 24 | Widget build(BuildContext context) { | 57 | 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; | 58 | + final size = _previewSize; |
| 30 | 59 | ||
| 31 | return SizedBox( | 60 | return SizedBox( |
| 32 | - width: w + knobWidth, | ||
| 33 | - height: h, | ||
| 34 | child: Stack( | 61 | child: Stack( |
| 35 | clipBehavior: Clip.none, | 62 | clipBehavior: Clip.none, |
| 36 | children: [ | 63 | 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 | - ), | 64 | + Image( |
| 65 | + image: AssetImage(_backgroundAsset), | ||
| 66 | + width: size.width, | ||
| 67 | + height: size.height, | ||
| 68 | + fit: BoxFit.fitWidth, | ||
| 48 | ), | 69 | ), |
| 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 | - ), | 70 | + _buildThemeImages(), |
| 71 | + ], | ||
| 72 | + ), | ||
| 73 | + ); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + Widget _buildThemeImages() { | ||
| 77 | + final themeSize = _themeSize; | ||
| 78 | + | ||
| 79 | + if (type == WatchFacePreviewType.singleMedium) { | ||
| 80 | + return Positioned( | ||
| 81 | + top: 40, | ||
| 82 | + width: themeSize.width, | ||
| 83 | + height: themeSize.height, | ||
| 84 | + child: _buildFaceImage(themeImageUrl, | ||
| 85 | + 'assets/images/watch_theme/official_default_green.png'), | ||
| 86 | + ); | ||
| 87 | + } | ||
| 88 | + if (type == WatchFacePreviewType.singleSmall) { | ||
| 89 | + return Positioned( | ||
| 90 | + top: 36, | ||
| 91 | + width: themeSize.width, | ||
| 92 | + height: themeSize.height, | ||
| 93 | + child: _buildFaceImage(themeImageUrl, | ||
| 94 | + 'assets/images/watch_theme/official_default_green.png'), | ||
| 95 | + ); | ||
| 96 | + } | ||
| 97 | + if (type == WatchFacePreviewType.surface) { | ||
| 98 | + return Positioned( | ||
| 99 | + top: 90, | ||
| 100 | + left: 24, | ||
| 101 | + width: themeSize.width, | ||
| 102 | + height: themeSize.height, | ||
| 103 | + child: _buildFaceImage(themeImageUrl, | ||
| 104 | + 'assets/images/watch_theme/official_default_green.png'), | ||
| 105 | + ); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + return Positioned( | ||
| 109 | + top: 30, | ||
| 110 | + child: Row( | ||
| 111 | + children: [ | ||
| 112 | + SizedBox( | ||
| 113 | + width: themeSize.width, | ||
| 114 | + height: themeSize.height, | ||
| 115 | + child: _buildFaceImage(themeImageUrl, | ||
| 116 | + 'assets/images/watch_theme/official_default_green.png'), | ||
| 75 | ), | 117 | ), |
| 118 | + SizedBox( | ||
| 119 | + width: 8, | ||
| 76 | ), | 120 | ), |
| 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 | - ), | 121 | + SizedBox( |
| 122 | + width: themeSize.width, | ||
| 123 | + height: themeSize.height, | ||
| 124 | + child: _buildFaceImage(themeImageUrl2, | ||
| 125 | + 'assets/images/watch_theme/official_default_blue.png'), | ||
| 90 | ), | 126 | ), |
| 91 | ], | 127 | ], |
| 92 | ), | 128 | ), |
| 93 | ); | 129 | ); |
| 94 | } | 130 | } |
| 95 | 131 | ||
| 96 | - Widget _buildFaceImage() { | ||
| 97 | - if (faceFilePath != null && faceFilePath!.isNotEmpty) { | ||
| 98 | - return Image.file(File(faceFilePath!), fit: BoxFit.cover); | 132 | + Widget _buildFaceImage(String? themeUrl, String placeholder) { |
| 133 | + if (themeUrl != null && themeUrl.isNotEmpty) { | ||
| 134 | + if (themeUrl.isURL) { | ||
| 135 | + return CachedNetworkImage(imageUrl: themeUrl); | ||
| 136 | + } | ||
| 137 | + return Image.file(File(themeUrl), fit: BoxFit.cover); | ||
| 99 | } | 138 | } |
| 100 | return Image.asset( | 139 | return Image.asset( |
| 101 | - faceAsset ?? R.assetsImagesWatchThemeFaceDefault, | 140 | + placeholder, |
| 102 | fit: BoxFit.cover, | 141 | fit: BoxFit.cover, |
| 103 | ); | 142 | ); |
| 104 | } | 143 | } |
| 1 | -import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 3 | 2 | ||
| 4 | import 'watch_face_preview.dart'; | 3 | import 'watch_face_preview.dart'; |
| @@ -8,31 +7,29 @@ class WatchThemeHeader extends StatelessWidget { | @@ -8,31 +7,29 @@ class WatchThemeHeader extends StatelessWidget { | ||
| 8 | const WatchThemeHeader({ | 7 | const WatchThemeHeader({ |
| 9 | super.key, | 8 | super.key, |
| 10 | this.title = '默认主题', | 9 | this.title = '默认主题', |
| 11 | - this.faceAsset, | ||
| 12 | - this.faceFilePath, | 10 | + this.themeImageUrl, |
| 13 | }); | 11 | }); |
| 14 | 12 | ||
| 15 | final String title; | 13 | final String title; |
| 16 | - final String? faceAsset; | ||
| 17 | - final String? faceFilePath; | 14 | + final String? themeImageUrl; |
| 18 | 15 | ||
| 19 | @override | 16 | @override |
| 20 | Widget build(BuildContext context) { | 17 | Widget build(BuildContext context) { |
| 21 | return Column( | 18 | return Column( |
| 22 | children: [ | 19 | children: [ |
| 23 | - SizedBox(height: 12.dp), | 20 | + SizedBox(height: 12), |
| 24 | Center( | 21 | Center( |
| 25 | child: WatchFacePreview( | 22 | child: WatchFacePreview( |
| 26 | - faceAsset: faceAsset, | ||
| 27 | - faceFilePath: faceFilePath, | 23 | + type: WatchFacePreviewType.singleMedium, |
| 24 | + themeImageUrl: themeImageUrl, | ||
| 28 | ), | 25 | ), |
| 29 | ), | 26 | ), |
| 30 | - SizedBox(height: 12.dp), | 27 | + SizedBox(height: 12), |
| 31 | Text( | 28 | Text( |
| 32 | title, | 29 | title, |
| 33 | style: TextStyle( | 30 | style: TextStyle( |
| 34 | color: WatchThemeColors.textPrimary, | 31 | color: WatchThemeColors.textPrimary, |
| 35 | - fontSize: 20.dp, | 32 | + fontSize: 20, |
| 36 | fontWeight: FontWeight.w600, | 33 | fontWeight: FontWeight.w600, |
| 37 | height: 1.25, | 34 | height: 1.25, |
| 38 | ), | 35 | ), |
| @@ -9,13 +9,11 @@ class WatchThemeSyncDialog extends StatefulWidget { | @@ -9,13 +9,11 @@ class WatchThemeSyncDialog extends StatefulWidget { | ||
| 9 | const WatchThemeSyncDialog({ | 9 | const WatchThemeSyncDialog({ |
| 10 | super.key, | 10 | super.key, |
| 11 | required this.onSync, | 11 | required this.onSync, |
| 12 | - this.faceAsset, | ||
| 13 | - this.faceFilePath, | 12 | + this.themeImageUrl, |
| 14 | }); | 13 | }); |
| 15 | 14 | ||
| 16 | final Future<bool> Function() onSync; | 15 | final Future<bool> Function() onSync; |
| 17 | - final String? faceAsset; | ||
| 18 | - final String? faceFilePath; | 16 | + final String? themeImageUrl; |
| 19 | 17 | ||
| 20 | @override | 18 | @override |
| 21 | State<WatchThemeSyncDialog> createState() => _WatchThemeSyncDialogState(); | 19 | State<WatchThemeSyncDialog> createState() => _WatchThemeSyncDialogState(); |
| @@ -76,10 +74,8 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> { | @@ -76,10 +74,8 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> { | ||
| 76 | right: 0, | 74 | right: 0, |
| 77 | child: Center( | 75 | child: Center( |
| 78 | child: WatchFacePreview( | 76 | child: WatchFacePreview( |
| 79 | - width: 134, | ||
| 80 | - height: 160, | ||
| 81 | - faceAsset: widget.faceAsset, | ||
| 82 | - faceFilePath: widget.faceFilePath, | 77 | + type: WatchFacePreviewType.singleMedium, |
| 78 | + themeImageUrl: widget.themeImageUrl, | ||
| 83 | ), | 79 | ), |
| 84 | ), | 80 | ), |
| 85 | ), | 81 | ), |
| 1 | import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; | 1 | import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; |
| 2 | import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart'; |
| 3 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 3 | import 'package:flutter/foundation.dart'; | 4 | import 'package:flutter/foundation.dart'; |
| 4 | 5 | ||
| 5 | import 'package:get/get.dart'; | 6 | import 'package:get/get.dart'; |
| @@ -65,6 +66,8 @@ abstract final class AppRoutes { | @@ -65,6 +66,8 @@ abstract final class AppRoutes { | ||
| 65 | abstract final class AppPages { | 66 | abstract final class AppPages { |
| 66 | static const initialRoute = AppRoutes.splash; | 67 | static const initialRoute = AppRoutes.splash; |
| 67 | 68 | ||
| 69 | + static final environmentConfig = Get.find<AppEnvironmentConfig>(); | ||
| 70 | + | ||
| 68 | static final routes = [ | 71 | static final routes = [ |
| 69 | GetPage( | 72 | GetPage( |
| 70 | name: AppRoutes.splash, | 73 | name: AppRoutes.splash, |
| @@ -81,7 +84,7 @@ abstract final class AppPages { | @@ -81,7 +84,7 @@ abstract final class AppPages { | ||
| 81 | page: () => const PhoneLoginView(), | 84 | page: () => const PhoneLoginView(), |
| 82 | binding: LoginBinding(), | 85 | binding: LoginBinding(), |
| 83 | ), | 86 | ), |
| 84 | - if (kDebugMode) | 87 | + if (environmentConfig.isDebug) |
| 85 | GetPage( | 88 | GetPage( |
| 86 | name: AppRoutes.debugEnvironment, | 89 | name: AppRoutes.debugEnvironment, |
| 87 | page: () => const DebugEnvironmentView(), | 90 | page: () => const DebugEnvironmentView(), |
| 1 | +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | ||
| 2 | +import 'package:flutter/foundation.dart'; | ||
| 1 | import 'package:get/get.dart'; | 3 | import 'package:get/get.dart'; |
| 2 | 4 | ||
| 3 | import '../constants/app_const.dart'; | 5 | import '../constants/app_const.dart'; |
| @@ -10,11 +12,13 @@ class AppEnvironmentConfig { | @@ -10,11 +12,13 @@ class AppEnvironmentConfig { | ||
| 10 | AppEnvironmentConfig(this._storage); | 12 | AppEnvironmentConfig(this._storage); |
| 11 | 13 | ||
| 12 | final LocalStorage _storage; | 14 | final LocalStorage _storage; |
| 15 | + final PlatformHostApi _platformHostApi = PlatformHostApi(); | ||
| 13 | 16 | ||
| 14 | final Rx<AppEnvironment> environment = AppEnvironment.release.obs; | 17 | final Rx<AppEnvironment> environment = AppEnvironment.release.obs; |
| 15 | 18 | ||
| 16 | Future<AppEnvironmentConfig> init() async { | 19 | Future<AppEnvironmentConfig> init() async { |
| 17 | environment.value = await _storage.readEnvironment(); | 20 | environment.value = await _storage.readEnvironment(); |
| 21 | + _isNativeDebug = await _platformHostApi.isDebugEnvoriment(); | ||
| 18 | return this; | 22 | return this; |
| 19 | } | 23 | } |
| 20 | 24 | ||
| @@ -23,6 +27,9 @@ class AppEnvironmentConfig { | @@ -23,6 +27,9 @@ class AppEnvironmentConfig { | ||
| 23 | await _storage.writeEnvironment(value); | 27 | await _storage.writeEnvironment(value); |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 30 | + bool _isNativeDebug = false; | ||
| 31 | + | ||
| 32 | + bool get isDebug => kDebugMode || _isNativeDebug; | ||
| 26 | String get serverBaseUrl => resolveServerUrl(AppConst.serverBaseUrl); | 33 | String get serverBaseUrl => resolveServerUrl(AppConst.serverBaseUrl); |
| 27 | 34 | ||
| 28 | String resolveServerUrl(String url) { | 35 | String resolveServerUrl(String url) { |
| @@ -30,6 +30,11 @@ bool _deepEquals(Object? a, Object? b) { | @@ -30,6 +30,11 @@ bool _deepEquals(Object? a, Object? b) { | ||
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | 32 | ||
| 33 | +enum HResourceType { | ||
| 34 | + image, | ||
| 35 | + video, | ||
| 36 | +} | ||
| 37 | + | ||
| 33 | class AppleSignInModel { | 38 | class AppleSignInModel { |
| 34 | AppleSignInModel({ | 39 | AppleSignInModel({ |
| 35 | required this.userId, | 40 | required this.userId, |
| @@ -305,17 +310,20 @@ class _PigeonCodec extends StandardMessageCodec { | @@ -305,17 +310,20 @@ class _PigeonCodec extends StandardMessageCodec { | ||
| 305 | if (value is int) { | 310 | if (value is int) { |
| 306 | buffer.putUint8(4); | 311 | buffer.putUint8(4); |
| 307 | buffer.putInt64(value); | 312 | buffer.putInt64(value); |
| 308 | - } else if (value is AppleSignInModel) { | 313 | + } else if (value is HResourceType) { |
| 309 | buffer.putUint8(129); | 314 | buffer.putUint8(129); |
| 315 | + writeValue(buffer, value.index); | ||
| 316 | + } else if (value is AppleSignInModel) { | ||
| 317 | + buffer.putUint8(130); | ||
| 310 | writeValue(buffer, value.encode()); | 318 | writeValue(buffer, value.encode()); |
| 311 | } else if (value is WatchAppOtherInfo) { | 319 | } else if (value is WatchAppOtherInfo) { |
| 312 | - buffer.putUint8(130); | 320 | + buffer.putUint8(131); |
| 313 | writeValue(buffer, value.encode()); | 321 | writeValue(buffer, value.encode()); |
| 314 | } else if (value is AppleProductInfo) { | 322 | } else if (value is AppleProductInfo) { |
| 315 | - buffer.putUint8(131); | 323 | + buffer.putUint8(132); |
| 316 | writeValue(buffer, value.encode()); | 324 | writeValue(buffer, value.encode()); |
| 317 | } else if (value is AppleProductPaymentResult) { | 325 | } else if (value is AppleProductPaymentResult) { |
| 318 | - buffer.putUint8(132); | 326 | + buffer.putUint8(133); |
| 319 | writeValue(buffer, value.encode()); | 327 | writeValue(buffer, value.encode()); |
| 320 | } else { | 328 | } else { |
| 321 | super.writeValue(buffer, value); | 329 | super.writeValue(buffer, value); |
| @@ -326,12 +334,15 @@ class _PigeonCodec extends StandardMessageCodec { | @@ -326,12 +334,15 @@ class _PigeonCodec extends StandardMessageCodec { | ||
| 326 | Object? readValueOfType(int type, ReadBuffer buffer) { | 334 | Object? readValueOfType(int type, ReadBuffer buffer) { |
| 327 | switch (type) { | 335 | switch (type) { |
| 328 | case 129: | 336 | case 129: |
| 329 | - return AppleSignInModel.decode(readValue(buffer)!); | 337 | + final int? value = readValue(buffer) as int?; |
| 338 | + return value == null ? null : HResourceType.values[value]; | ||
| 330 | case 130: | 339 | case 130: |
| 331 | - return WatchAppOtherInfo.decode(readValue(buffer)!); | 340 | + return AppleSignInModel.decode(readValue(buffer)!); |
| 332 | case 131: | 341 | case 131: |
| 333 | - return AppleProductInfo.decode(readValue(buffer)!); | 342 | + return WatchAppOtherInfo.decode(readValue(buffer)!); |
| 334 | case 132: | 343 | case 132: |
| 344 | + return AppleProductInfo.decode(readValue(buffer)!); | ||
| 345 | + case 133: | ||
| 335 | return AppleProductPaymentResult.decode(readValue(buffer)!); | 346 | return AppleProductPaymentResult.decode(readValue(buffer)!); |
| 336 | default: | 347 | default: |
| 337 | return super.readValueOfType(type, buffer); | 348 | return super.readValueOfType(type, buffer); |
| @@ -382,6 +393,35 @@ class PlatformHostApi { | @@ -382,6 +393,35 @@ class PlatformHostApi { | ||
| 382 | } | 393 | } |
| 383 | } | 394 | } |
| 384 | 395 | ||
| 396 | + /// 是否是测试环境 | ||
| 397 | + Future<bool> isDebugEnvoriment() async { | ||
| 398 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$pigeonVar_messageChannelSuffix'; | ||
| 399 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 400 | + pigeonVar_channelName, | ||
| 401 | + pigeonChannelCodec, | ||
| 402 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 403 | + ); | ||
| 404 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 405 | + final List<Object?>? pigeonVar_replyList = | ||
| 406 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 407 | + if (pigeonVar_replyList == null) { | ||
| 408 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 409 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 410 | + throw PlatformException( | ||
| 411 | + code: pigeonVar_replyList[0]! as String, | ||
| 412 | + message: pigeonVar_replyList[1] as String?, | ||
| 413 | + details: pigeonVar_replyList[2], | ||
| 414 | + ); | ||
| 415 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 416 | + throw PlatformException( | ||
| 417 | + code: 'null-error', | ||
| 418 | + message: 'Host platform returned null value for non-null return value.', | ||
| 419 | + ); | ||
| 420 | + } else { | ||
| 421 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 422 | + } | ||
| 423 | + } | ||
| 424 | + | ||
| 385 | /// 更新用户信息, 有登录态后调用 | 425 | /// 更新用户信息, 有登录态后调用 |
| 386 | /// jsonString: UserPreferences的序列化string | 426 | /// jsonString: UserPreferences的序列化string |
| 387 | /// baseUrl: 请求地址, https://api.doublefeel.cn | 427 | /// baseUrl: 请求地址, https://api.doublefeel.cn |
| @@ -456,7 +496,7 @@ class PlatformHostApi { | @@ -456,7 +496,7 @@ class PlatformHostApi { | ||
| 456 | } | 496 | } |
| 457 | } | 497 | } |
| 458 | 498 | ||
| 459 | - /// 刷新watch app 和 表盘的所有数据: | 499 | + /// 刷新watch app 和 表盘的所有数据: |
| 460 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) | 500 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) |
| 461 | Future<void> refreshWatchAppAndWidgets() async { | 501 | Future<void> refreshWatchAppAndWidgets() async { |
| 462 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets$pigeonVar_messageChannelSuffix'; | 502 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets$pigeonVar_messageChannelSuffix'; |
| @@ -481,6 +521,35 @@ class PlatformHostApi { | @@ -481,6 +521,35 @@ class PlatformHostApi { | ||
| 481 | } | 521 | } |
| 482 | } | 522 | } |
| 483 | 523 | ||
| 524 | + /// 请求评分弹窗 | ||
| 525 | + Future<bool> requestAppReview() async { | ||
| 526 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$pigeonVar_messageChannelSuffix'; | ||
| 527 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 528 | + pigeonVar_channelName, | ||
| 529 | + pigeonChannelCodec, | ||
| 530 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 531 | + ); | ||
| 532 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 533 | + final List<Object?>? pigeonVar_replyList = | ||
| 534 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 535 | + if (pigeonVar_replyList == null) { | ||
| 536 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 537 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 538 | + throw PlatformException( | ||
| 539 | + code: pigeonVar_replyList[0]! as String, | ||
| 540 | + message: pigeonVar_replyList[1] as String?, | ||
| 541 | + details: pigeonVar_replyList[2], | ||
| 542 | + ); | ||
| 543 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 544 | + throw PlatformException( | ||
| 545 | + code: 'null-error', | ||
| 546 | + message: 'Host platform returned null value for non-null return value.', | ||
| 547 | + ); | ||
| 548 | + } else { | ||
| 549 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 550 | + } | ||
| 551 | + } | ||
| 552 | + | ||
| 484 | /// 请求苹果登录 | 553 | /// 请求苹果登录 |
| 485 | Future<AppleSignInModel?> requestAppleSignIn() async { | 554 | Future<AppleSignInModel?> requestAppleSignIn() async { |
| 486 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix'; | 555 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix'; |
| @@ -583,4 +652,29 @@ class PlatformHostApi { | @@ -583,4 +652,29 @@ class PlatformHostApi { | ||
| 583 | return (pigeonVar_replyList[0] as bool?)!; | 652 | return (pigeonVar_replyList[0] as bool?)!; |
| 584 | } | 653 | } |
| 585 | } | 654 | } |
| 655 | + | ||
| 656 | + /// 上传文件到云端 | ||
| 657 | + /// 注意catch flutter error | ||
| 658 | + Future<String?> uploadFile(String filePath, HResourceType resourceType) async { | ||
| 659 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$pigeonVar_messageChannelSuffix'; | ||
| 660 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 661 | + pigeonVar_channelName, | ||
| 662 | + pigeonChannelCodec, | ||
| 663 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 664 | + ); | ||
| 665 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[filePath, resourceType]); | ||
| 666 | + final List<Object?>? pigeonVar_replyList = | ||
| 667 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 668 | + if (pigeonVar_replyList == null) { | ||
| 669 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 670 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 671 | + throw PlatformException( | ||
| 672 | + code: pigeonVar_replyList[0]! as String, | ||
| 673 | + message: pigeonVar_replyList[1] as String?, | ||
| 674 | + details: pigeonVar_replyList[2], | ||
| 675 | + ); | ||
| 676 | + } else { | ||
| 677 | + return (pigeonVar_replyList[0] as String?); | ||
| 678 | + } | ||
| 679 | + } | ||
| 586 | } | 680 | } |
| @@ -280,4 +280,33 @@ class WearEngineHostApi { | @@ -280,4 +280,33 @@ class WearEngineHostApi { | ||
| 280 | return (pigeonVar_replyList[0] as String?); | 280 | return (pigeonVar_replyList[0] as String?); |
| 281 | } | 281 | } |
| 282 | } | 282 | } |
| 283 | + | ||
| 284 | + /// 是否有已安装的表盘 | ||
| 285 | + Future<bool> hasInstalledWatchSurface() async { | ||
| 286 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$pigeonVar_messageChannelSuffix'; | ||
| 287 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 288 | + pigeonVar_channelName, | ||
| 289 | + pigeonChannelCodec, | ||
| 290 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 291 | + ); | ||
| 292 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 293 | + final List<Object?>? pigeonVar_replyList = | ||
| 294 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 295 | + if (pigeonVar_replyList == null) { | ||
| 296 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 297 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 298 | + throw PlatformException( | ||
| 299 | + code: pigeonVar_replyList[0]! as String, | ||
| 300 | + message: pigeonVar_replyList[1] as String?, | ||
| 301 | + details: pigeonVar_replyList[2], | ||
| 302 | + ); | ||
| 303 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 304 | + throw PlatformException( | ||
| 305 | + code: 'null-error', | ||
| 306 | + message: 'Host platform returned null value for non-null return value.', | ||
| 307 | + ); | ||
| 308 | + } else { | ||
| 309 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 310 | + } | ||
| 311 | + } | ||
| 283 | } | 312 | } |
| @@ -40,36 +40,6 @@ class R { | @@ -40,36 +40,6 @@ class R { | ||
| 40 | static final String assetsImagesMyAvatarDefault = | 40 | static final String assetsImagesMyAvatarDefault = |
| 41 | 'assets/images/my/avatar_default.png'; | 41 | 'assets/images/my/avatar_default.png'; |
| 42 | 42 | ||
| 43 | - // watch theme | ||
| 44 | - static final String assetsImagesWatchThemeFaceDefault = | ||
| 45 | - 'assets/images/watch_theme/watch_face_default.png'; | ||
| 46 | - static final String assetsImagesWatchThemeFacePreviewAlt = | ||
| 47 | - 'assets/images/watch_theme/watch_face_preview_alt.png'; | ||
| 48 | - static final String assetsImagesWatchThemeCustomFaceCreate = | ||
| 49 | - 'assets/images/watch_theme/custom_watch_face_create.png'; | ||
| 50 | - static final String assetsImagesWatchThemeCustomFacePreview = | ||
| 51 | - 'assets/images/watch_theme/custom_watch_face_preview.png'; | ||
| 52 | - static final String assetsImagesWatchThemeCustomPreviewAlt = | ||
| 53 | - 'assets/images/watch_theme/custom_preview_alt.png'; | ||
| 54 | - static final String assetsImagesWatchThemeCustomStatusExcellent = | ||
| 55 | - 'assets/images/watch_theme/custom_status_excellent.png'; | ||
| 56 | - static final String assetsImagesWatchThemeCustomStatusNormal = | ||
| 57 | - 'assets/images/watch_theme/custom_status_normal.png'; | ||
| 58 | - static final String assetsImagesWatchThemeCustomStatusStress = | ||
| 59 | - 'assets/images/watch_theme/custom_status_stress.png'; | ||
| 60 | - static final String assetsImagesWatchThemeCustomStatusOverload = | ||
| 61 | - 'assets/images/watch_theme/custom_status_overload.png'; | ||
| 62 | - static final String assetsImagesWatchThemeOfficialDogWhite = | ||
| 63 | - 'assets/images/watch_theme/official_dog_white.png'; | ||
| 64 | - static final String assetsImagesWatchThemeOfficialRabbitPink = | ||
| 65 | - 'assets/images/watch_theme/official_rabbit_pink.png'; | ||
| 66 | - static final String assetsImagesWatchThemeOfficialCatOrange = | ||
| 67 | - 'assets/images/watch_theme/official_cat_orange.png'; | ||
| 68 | - static final String assetsImagesWatchThemeOfficialElephantBlue = | ||
| 69 | - 'assets/images/watch_theme/official_elephant_blue.png'; | ||
| 70 | - static final String assetsImagesWatchThemeCustomCatDog = | ||
| 71 | - 'assets/images/watch_theme/custom_cat_dog.png'; | ||
| 72 | - | ||
| 73 | // real-time stress | 43 | // real-time stress |
| 74 | static final String assetsImagesRealtimeStressAttentionIcon = | 44 | static final String assetsImagesRealtimeStressAttentionIcon = |
| 75 | 'assets/images/common/ic_health_stress_attention.webp'; | 45 | 'assets/images/common/ic_health_stress_attention.webp'; |
| @@ -116,6 +116,9 @@ abstract class PlatformHostApi { | @@ -116,6 +116,9 @@ abstract class PlatformHostApi { | ||
| 116 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 116 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 117 | String getFullUserAgent(); | 117 | String getFullUserAgent(); |
| 118 | 118 | ||
| 119 | + /// 是否是测试环境 | ||
| 120 | + bool isDebugEnvoriment(); | ||
| 121 | + | ||
| 119 | /// 更新用户信息, 有登录态后调用 | 122 | /// 更新用户信息, 有登录态后调用 |
| 120 | /// jsonString: UserPreferences的序列化string | 123 | /// jsonString: UserPreferences的序列化string |
| 121 | /// baseUrl: 请求地址, https://api.doublefeel.cn | 124 | /// baseUrl: 请求地址, https://api.doublefeel.cn |
| @@ -131,6 +134,10 @@ abstract class PlatformHostApi { | @@ -131,6 +134,10 @@ abstract class PlatformHostApi { | ||
| 131 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) | 134 | /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) |
| 132 | void refreshWatchAppAndWidgets(); | 135 | void refreshWatchAppAndWidgets(); |
| 133 | 136 | ||
| 137 | + /// 请求评分弹窗 | ||
| 138 | + @async | ||
| 139 | + bool requestAppReview(); | ||
| 140 | + | ||
| 134 | /// 请求苹果登录 | 141 | /// 请求苹果登录 |
| 135 | @async | 142 | @async |
| 136 | AppleSignInModel? requestAppleSignIn(); | 143 | AppleSignInModel? requestAppleSignIn(); |
| @@ -40,9 +40,12 @@ abstract class WearEngineHostApi { | @@ -40,9 +40,12 @@ abstract class WearEngineHostApi { | ||
| 40 | 40 | ||
| 41 | bool sendWatchSyncPayload(String jsonPayload); | 41 | bool sendWatchSyncPayload(String jsonPayload); |
| 42 | 42 | ||
| 43 | - | ||
| 44 | /// Opens the system photo picker and returns a local PNG file path after | 43 | /// Opens the system photo picker and returns a local PNG file path after |
| 45 | /// removing the image background on the host platform. | 44 | /// removing the image background on the host platform. |
| 46 | @async | 45 | @async |
| 47 | String? removeBackground(String originImagePath); | 46 | String? removeBackground(String originImagePath); |
| 47 | + | ||
| 48 | + /// 是否有已安装的表盘 | ||
| 49 | + @async | ||
| 50 | + bool hasInstalledWatchSurface(); | ||
| 48 | } | 51 | } |
| @@ -430,13 +430,13 @@ packages: | @@ -430,13 +430,13 @@ packages: | ||
| 430 | source: hosted | 430 | source: hosted |
| 431 | version: "6.1.0" | 431 | version: "6.1.0" |
| 432 | image_cropper_platform_interface: | 432 | image_cropper_platform_interface: |
| 433 | - dependency: transitive | 433 | + dependency: "direct overridden" |
| 434 | description: | 434 | description: |
| 435 | name: image_cropper_platform_interface | 435 | name: image_cropper_platform_interface |
| 436 | - sha256: "2d8db8f4b638e448fa89a1e77cd8f053b4547472bd3ae073169e86626d03afef" | 436 | + sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af" |
| 437 | url: "https://pub.dev" | 437 | url: "https://pub.dev" |
| 438 | source: hosted | 438 | source: hosted |
| 439 | - version: "7.2.0" | 439 | + version: "7.1.0" |
| 440 | image_picker: | 440 | image_picker: |
| 441 | dependency: "direct main" | 441 | dependency: "direct main" |
| 442 | description: | 442 | description: |
| @@ -647,7 +647,7 @@ packages: | @@ -647,7 +647,7 @@ packages: | ||
| 647 | source: hosted | 647 | source: hosted |
| 648 | version: "1.9.1" | 648 | version: "1.9.1" |
| 649 | path_provider: | 649 | path_provider: |
| 650 | - dependency: "direct overridden" | 650 | + dependency: "direct main" |
| 651 | description: | 651 | description: |
| 652 | name: path_provider | 652 | name: path_provider |
| 653 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" | 653 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" |
| @@ -51,6 +51,7 @@ dependencies: | @@ -51,6 +51,7 @@ dependencies: | ||
| 51 | video_thumbnail: ^0.5.3 | 51 | video_thumbnail: ^0.5.3 |
| 52 | table_calendar: ^3.1.3 | 52 | table_calendar: ^3.1.3 |
| 53 | fluttertoast: ^8.2.2 | 53 | fluttertoast: ^8.2.2 |
| 54 | + path_provider: ^2.1.5 | ||
| 54 | flutter_localizations: | 55 | flutter_localizations: |
| 55 | sdk: flutter | 56 | sdk: flutter |
| 56 | intl: any | 57 | intl: any |
| @@ -60,6 +61,7 @@ dependencies: | @@ -60,6 +61,7 @@ dependencies: | ||
| 60 | # 强制使用 hosted 版本统一来源。 | 61 | # 强制使用 hosted 版本统一来源。 |
| 61 | dependency_overrides: | 62 | dependency_overrides: |
| 62 | path_provider: ^2.1.5 | 63 | path_provider: ^2.1.5 |
| 64 | + image_cropper_platform_interface: 7.1.0 | ||
| 63 | 65 | ||
| 64 | dev_dependencies: | 66 | dev_dependencies: |
| 65 | flutter_test: | 67 | flutter_test: |
-
Please register or login to post a comment