Commit 87e39b965578ac2ab9ee475cf30a79d698bf109c

Authored by 权海
1 parent cb2e56ad

feat(ui):增加bridge一些接口,增加原生的debug环境回传flutter(切环境)

Showing 48 changed files with 768 additions and 344 deletions
... ... @@ -54,6 +54,12 @@
@import sqflite_darwin;
#endif
#if __has_include(<video_thumbnail/VideoThumbnailPlugin.h>)
#import <video_thumbnail/VideoThumbnailPlugin.h>
#else
@import video_thumbnail;
#endif
#if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>)
#import <webview_flutter_wkwebview/WebViewFlutterPlugin.h>
#else
... ... @@ -71,6 +77,7 @@
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[VideoThumbnailPlugin registerWithRegistrar:[registry registrarForPlugin:@"VideoThumbnailPlugin"]];
[WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]];
}
... ...
... ... @@ -79,6 +79,17 @@ class FlutterError (
val details: Any? = null
) : Throwable()
enum class HResourceType(val raw: Int) {
IMAGE(0),
VIDEO(1);
companion object {
fun ofRaw(raw: Int): HResourceType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class AppleSignInModel (
val userId: String,
... ... @@ -265,21 +276,26 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let {
HResourceType.ofRaw(it.toInt())
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AppleSignInModel.fromList(it)
}
}
130.toByte() -> {
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
WatchAppOtherInfo.fromList(it)
}
}
131.toByte() -> {
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AppleProductInfo.fromList(it)
}
}
132.toByte() -> {
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AppleProductPaymentResult.fromList(it)
}
... ... @@ -289,20 +305,24 @@ private open class PlatformApiPigeonCodec : StandardMessageCodec() {
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is AppleSignInModel -> {
is HResourceType -> {
stream.write(129)
writeValue(stream, value.raw)
}
is AppleSignInModel -> {
stream.write(130)
writeValue(stream, value.toList())
}
is WatchAppOtherInfo -> {
stream.write(130)
stream.write(131)
writeValue(stream, value.toList())
}
is AppleProductInfo -> {
stream.write(131)
stream.write(132)
writeValue(stream, value.toList())
}
is AppleProductPaymentResult -> {
stream.write(132)
stream.write(133)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
... ... @@ -318,6 +338,8 @@ interface PlatformHostApi {
* `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
*/
fun getFullUserAgent(): String
/** 是否是测试环境 */
fun isDebugEnvoriment(): Boolean
/**
* 更新用户信息, 有登录态后调用
* jsonString: UserPreferences的序列化string
... ... @@ -329,10 +351,12 @@ interface PlatformHostApi {
/** 刷新会员信息 */
fun refreshVip()
/**
* 刷新watch app 和 表盘的所有数据:
* 刷新watch app 和 表盘的所有数据:
* 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
*/
fun refreshWatchAppAndWidgets()
/** 请求评分弹窗 */
fun requestAppReview(callback: (Result<Boolean>) -> Unit)
/** 请求苹果登录 */
fun requestAppleSignIn(callback: (Result<AppleSignInModel?>) -> Unit)
/**
... ... @@ -345,6 +369,11 @@ interface PlatformHostApi {
fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit)
/** 恢复购买 */
fun performRestore(callback: (Result<Boolean>) -> Unit)
/**
* 上传文件到云端
* 注意catch flutter error
*/
fun uploadFile(filePath: String, resourceType: HResourceType, callback: (Result<String?>) -> Unit)
companion object {
/** The codec used by PlatformHostApi. */
... ... @@ -371,6 +400,21 @@ interface PlatformHostApi {
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isDebugEnvoriment())
} catch (exception: Throwable) {
PlatformApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
... ... @@ -438,6 +482,24 @@ interface PlatformHostApi {
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.requestAppReview{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(PlatformApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(PlatformApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
... ... @@ -515,6 +577,27 @@ interface PlatformHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val filePathArg = args[0] as String
val resourceTypeArg = args[1] as HResourceType
api.uploadFile(filePathArg, resourceTypeArg) { result: Result<String?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(PlatformApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(PlatformApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
... ...
... ... @@ -147,6 +147,8 @@ interface WearEngineHostApi {
* removing the image background on the host platform.
*/
fun removeBackground(originImagePath: String, callback: (Result<String?>) -> Unit)
/** 是否有已安装的表盘 */
fun hasInstalledWatchSurface(callback: (Result<Boolean>) -> Unit)
companion object {
/** The codec used by WearEngineHostApi. */
... ... @@ -256,6 +258,24 @@ interface WearEngineHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.hasInstalledWatchSurface{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(WearEngineApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(WearEngineApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
... ...
... ... @@ -112,6 +112,11 @@ func deepHashPlatformApi(value: Any?, hasher: inout Hasher) {
enum HResourceType: Int {
case image = 0
case video = 1
}
/// Generated class from Pigeon that represents data sent in messages.
struct AppleSignInModel: Hashable {
var userId: String
... ... @@ -301,12 +306,18 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
return AppleSignInModel.fromList(self.readValue() as! [Any?])
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return HResourceType(rawValue: enumResultAsInt)
}
return nil
case 130:
return WatchAppOtherInfo.fromList(self.readValue() as! [Any?])
return AppleSignInModel.fromList(self.readValue() as! [Any?])
case 131:
return AppleProductInfo.fromList(self.readValue() as! [Any?])
return WatchAppOtherInfo.fromList(self.readValue() as! [Any?])
case 132:
return AppleProductInfo.fromList(self.readValue() as! [Any?])
case 133:
return AppleProductPaymentResult.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
... ... @@ -316,17 +327,20 @@ private class PlatformApiPigeonCodecReader: FlutterStandardReader {
private class PlatformApiPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? AppleSignInModel {
if let value = value as? HResourceType {
super.writeByte(129)
super.writeValue(value.rawValue)
} else if let value = value as? AppleSignInModel {
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? WatchAppOtherInfo {
super.writeByte(130)
super.writeByte(131)
super.writeValue(value.toList())
} else if let value = value as? AppleProductInfo {
super.writeByte(131)
super.writeByte(132)
super.writeValue(value.toList())
} else if let value = value as? AppleProductPaymentResult {
super.writeByte(132)
super.writeByte(133)
super.writeValue(value.toList())
} else {
super.writeValue(value)
... ... @@ -354,6 +368,8 @@ protocol PlatformHostApi {
/// 返回完整的 User-Agent 字符串,由 native 侧组装:
/// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
func getFullUserAgent() throws -> String
/// 是否是测试环境
func isDebugEnvoriment() throws -> Bool
/// 更新用户信息, 有登录态后调用
/// jsonString: UserPreferences的序列化string
/// baseUrl: 请求地址, https://api.doublefeel.cn
... ... @@ -362,9 +378,11 @@ protocol PlatformHostApi {
func logout() throws
/// 刷新会员信息
func refreshVip() throws
/// 刷新watch app 和 表盘的所有数据:
/// 刷新watch app 和 表盘的所有数据:
/// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
func refreshWatchAppAndWidgets() throws
/// 请求评分弹窗
func requestAppReview(completion: @escaping (Result<Bool, Error>) -> Void)
/// 请求苹果登录
func requestAppleSignIn(completion: @escaping (Result<AppleSignInModel?, Error>) -> Void)
/// 查询指定id的苹果商品
... ... @@ -375,6 +393,9 @@ protocol PlatformHostApi {
func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void)
/// 恢复购买
func performRestore(completion: @escaping (Result<Bool, Error>) -> Void)
/// 上传文件到云端
/// 注意catch flutter error
func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
... ... @@ -398,6 +419,20 @@ class PlatformHostApiSetup {
} else {
getFullUserAgentChannel.setMessageHandler(nil)
}
/// 是否是测试环境
let isDebugEnvorimentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
isDebugEnvorimentChannel.setMessageHandler { _, reply in
do {
let result = try api.isDebugEnvoriment()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
isDebugEnvorimentChannel.setMessageHandler(nil)
}
/// 更新用户信息, 有登录态后调用
/// jsonString: UserPreferences的序列化string
/// baseUrl: 请求地址, https://api.doublefeel.cn
... ... @@ -445,7 +480,7 @@ class PlatformHostApiSetup {
} else {
refreshVipChannel.setMessageHandler(nil)
}
/// 刷新watch app 和 表盘的所有数据:
/// 刷新watch app 和 表盘的所有数据:
/// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
let refreshWatchAppAndWidgetsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
... ... @@ -460,6 +495,22 @@ class PlatformHostApiSetup {
} else {
refreshWatchAppAndWidgetsChannel.setMessageHandler(nil)
}
/// 请求评分弹窗
let requestAppReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
requestAppReviewChannel.setMessageHandler { _, reply in
api.requestAppReview { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
requestAppReviewChannel.setMessageHandler(nil)
}
/// 请求苹果登录
let requestAppleSignInChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
... ... @@ -532,5 +583,25 @@ class PlatformHostApiSetup {
} else {
performRestoreChannel.setMessageHandler(nil)
}
/// 上传文件到云端
/// 注意catch flutter error
let uploadFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
uploadFileChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let filePathArg = args[0] as! String
let resourceTypeArg = args[1] as! HResourceType
api.uploadFile(filePath: filePathArg, resourceType: resourceTypeArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
uploadFileChannel.setMessageHandler(nil)
}
}
}
... ...
... ... @@ -37,6 +37,18 @@ class PriceFormatter{
* `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)`
*/
final class PlatformHostApiImpl: PlatformHostApi {
func isDebugEnvoriment() throws -> Bool {
return true
}
func requestAppReview(completion: @escaping (Result<Bool, any Error>) -> Void) {
}
func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) {
}
func logout() throws {
}
... ...
... ... @@ -192,6 +192,8 @@ protocol WearEngineHostApi {
/// Opens the system photo picker and returns a local PNG file path after
/// removing the image background on the host platform.
func removeBackground(originImagePath: String, completion: @escaping (Result<String?, Error>) -> Void)
/// 是否有已安装的表盘
func hasInstalledWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
... ... @@ -288,5 +290,21 @@ class WearEngineHostApiSetup {
} else {
removeBackgroundChannel.setMessageHandler(nil)
}
/// 是否有已安装的表盘
let hasInstalledWatchSurfaceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
hasInstalledWatchSurfaceChannel.setMessageHandler { _, reply in
api.hasInstalledWatchSurface { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
hasInstalledWatchSurfaceChannel.setMessageHandler(nil)
}
}
}
... ...
... ... @@ -8,6 +8,10 @@ enum ImageProcessError: Error{
}
final class WearEngineHostApiImpl: WearEngineHostApi {
func hasInstalledWatchSurface(completion: @escaping (Result<Bool, any Error>) -> Void) {
}
private let watchService: WatchConnectivityService
init(watchService: WatchConnectivityService = .shared) {
... ...
... ... @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_v
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
... ... @@ -24,6 +25,7 @@ class MyTab extends GetView<MyController> {
@override
Widget build(BuildContext context) {
final userPrefs = Get.find<UserPreferencesStorage>();
final environmentConfig = Get.find<AppEnvironmentConfig>();
return Container(
color: context.colors.backgroundPage,
... ... @@ -74,7 +76,7 @@ class MyTab extends GetView<MyController> {
controller.testAppleHealthUpload();
},
),
if (kDebugMode) ...[
if (environmentConfig.isDebug) ...[
const SizedBox(height: 12),
_SettingsRow(
title: 'Route List',
... ...
... ... @@ -26,6 +26,7 @@ class LoginController extends GetxController {
final UserPreferencesStorage _userPrefs = Get.find<UserPreferencesStorage>();
final UserAccountStorage _userAccount = Get.find<UserAccountStorage>();
final UserStateService _userStateService = Get.find<UserStateService>();
final environmentConfig = Get.find<AppEnvironmentConfig>();
final phoneController = TextEditingController();
final codeController = TextEditingController();
... ...
... ... @@ -178,7 +178,7 @@ class LoginView extends GetView<LoginController> {
child: const _AgreementText(),
),
if (kDebugMode)
if (controller.environmentConfig.isDebug)
Positioned(
left: 0,
right: 0,
... ...
import 'dart:async';
import 'dart:io';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import '../models/watch_theme_models.dart';
import '../widgets/watch_theme_dialogs.dart';
... ... @@ -14,13 +20,18 @@ class CreateWatchThemeController extends GetxController {
CreateWatchThemeController(this._themeApi);
final ThemeApi _themeApi;
final PlatformHostApi _platformHostApi = PlatformHostApi();
final WearEngineHostApi _wearEngineHostApi = WearEngineHostApi();
final ImagePicker _imagePicker = ImagePicker();
final customThemeName = ''.obs;
final agreedToSubmission = true.obs;
final customStatusNames = ['状态优秀', '状态正常', '注意压力', '压力过载'].obs;
final customImagePaths = RxList<String?>.filled(4, null);
final isRemovingBackground = RxList<bool>.filled(4, false);
final isSaving = false.obs;
final List<int> _imageTaskVersions = List<int>.filled(4, 0);
bool _isClosed = false;
late final TextEditingController themeNameController;
@override
... ... @@ -34,6 +45,7 @@ class CreateWatchThemeController extends GetxController {
@override
void onClose() {
_isClosed = true;
themeNameController.dispose();
super.onClose();
}
... ... @@ -42,15 +54,136 @@ class CreateWatchThemeController extends GetxController {
!isSaving.value &&
customThemeName.value.isNotEmpty &&
agreedToSubmission.value &&
!isRemovingBackground.any((isProcessing) => isProcessing) &&
customImagePaths.every((path) => path != null && path.isNotEmpty);
Future<void> pickCustomImage(int index) async {
final picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
customImagePaths[index] = image.path;
} else {
AppToast.show('图片选择失败');
if (index < 0 || index >= customImagePaths.length) {
return;
}
try {
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) {
return;
}
final croppedImage = await ImageCropper().cropImage(
sourcePath: image.path,
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
maxWidth: 280,
maxHeight: 280,
compressFormat: ImageCompressFormat.png,
compressQuality: 100,
uiSettings: [
AndroidUiSettings(
toolbarTitle: '裁剪表盘图片',
lockAspectRatio: true,
),
IOSUiSettings(
title: '裁剪表盘图片',
aspectRatioLockEnabled: true,
resetAspectRatioEnabled: false,
),
],
);
if (croppedImage == null) {
return;
}
final savedPath = await _saveCroppedImage(croppedImage.path, index);
await _deleteReplacedImage(customImagePaths[index], savedPath);
customImagePaths[index] = savedPath;
final taskVersion = ++_imageTaskVersions[index];
isRemovingBackground[index] = true;
// Pigeon 方法是异步接口,iOS 端的 Vision 请求在 Task.detached 中执行。
// 此处不等待结果,让裁切图先显示;处理完成后再原位替换。
unawaited(_removeBackgroundAndReplace(index, savedPath, taskVersion));
} catch (_) {
AppToast.show('图片处理失败,请重试');
}
}
Future<void> _removeBackgroundAndReplace(
int index,
String croppedImagePath,
int taskVersion,
) async {
String? savedProcessedPath;
try {
final processedPath =
await _wearEngineHostApi.removeBackground(croppedImagePath);
if (processedPath == null || processedPath.isEmpty) {
return;
}
savedProcessedPath = await _saveProcessedImage(processedPath, index);
if (!_isCurrentImageTask(index, croppedImagePath, taskVersion)) {
await _deleteReplacedImage(savedProcessedPath, '');
return;
}
customImagePaths[index] = savedProcessedPath;
await _deleteReplacedImage(croppedImagePath, savedProcessedPath);
} catch (error, stackTrace) {
debugPrint('Watch theme background removal failed: $error');
debugPrintStack(stackTrace: stackTrace);
} finally {
if (!_isClosed && _imageTaskVersions[index] == taskVersion) {
isRemovingBackground[index] = false;
}
}
}
bool _isCurrentImageTask(
int index,
String croppedImagePath,
int taskVersion,
) {
return !_isClosed &&
_imageTaskVersions[index] == taskVersion &&
customImagePaths[index] == croppedImagePath;
}
Future<String> _saveCroppedImage(String sourcePath, int index) async {
return _saveThemeImage(sourcePath, index, 'cropped');
}
Future<String> _saveProcessedImage(String sourcePath, int index) async {
return _saveThemeImage(sourcePath, index, 'processed');
}
Future<String> _saveThemeImage(
String sourcePath,
int index,
String stage,
) async {
final documentsDirectory = await getApplicationDocumentsDirectory();
final themeImageDirectory =
Directory('${documentsDirectory.path}/watch_theme');
await themeImageDirectory.create(recursive: true);
final fileName = 'watch_theme_${index}_${stage}_'
'${DateTime.now().microsecondsSinceEpoch}.png';
final savedFile = await File(sourcePath).copy(
'${themeImageDirectory.path}/$fileName',
);
return savedFile.path;
}
Future<void> _deleteReplacedImage(
String? previousPath,
String replacementPath,
) async {
if (previousPath == null || previousPath == replacementPath) {
return;
}
final previousFile = File(previousPath);
if (previousFile.parent.path.endsWith('/watch_theme') &&
await previousFile.exists()) {
await previousFile.delete();
}
}
... ... @@ -95,73 +228,61 @@ class CreateWatchThemeController extends GetxController {
}
isSaving.value = true;
var imagePath1 = customImagePaths[0];
var imagePath2 = customImagePaths[1];
var imagePath3 = customImagePaths[2];
var imagePath4 = customImagePaths[3];
if (imagePath1 != null) {
final path = await _wearEngineHostApi.removeBackground(imagePath1);
if (path != null) {
imagePath1 = path;
}
}
if (imagePath2 != null) {
final path = await _wearEngineHostApi.removeBackground(imagePath2);
if (path != null) {
imagePath2 = path;
}
}
if (imagePath3 != null) {
final path = await _wearEngineHostApi.removeBackground(imagePath3);
if (path != null) {
imagePath3 = path;
try {
final uploadedImageUrls = <String>[];
for (final imagePath in customImagePaths) {
final imageUrl = await _platformHostApi.uploadFile(
imagePath!,
HResourceType.image,
);
if (imageUrl == null || imageUrl.isEmpty) {
AppToast.show('图片上传失败,请重试');
return;
}
uploadedImageUrls.add(imageUrl);
}
}
if (imagePath4 != null) {
final path = await _wearEngineHostApi.removeBackground(imagePath4);
if (path != null) {
imagePath4 = path;
final result = await _themeApi.createTheme(
themeName: customThemeName.value,
energeticDescription: customStatusNames[0],
energeticImage: uploadedImageUrls[0],
normalDescription: customStatusNames[1],
normalImage: uploadedImageUrls[1],
slightStressfulDescription: customStatusNames[2],
slightStressfulImage: uploadedImageUrls[2],
stressfulDescription: customStatusNames[3],
stressfulImage: uploadedImageUrls[3],
);
if (result is AppFailure<void>) {
AppToast.show(result.error.displayMessage);
return;
}
}
//TODO: - 上传图片
// final result = await _themeApi.createTheme(
// themeName: customThemeName.value,
// energeticDescription: customStatusNames[0],
// energeticImage: customImagePaths[0] ?? '',
// normalDescription: customStatusNames[1],
// normalImage: customImagePaths[1] ?? '',
// slightStressfulDescription: customStatusNames[2],
// slightStressfulImage: customImagePaths[2] ?? '',
// stressfulDescription: customStatusNames[3],
// stressfulImage: customImagePaths[3] ?? '',
// );
isSaving.value = false;
//Test: - 测试抠图结果
customImagePaths.value = [imagePath1, imagePath2, imagePath3, imagePath4];
customImagePaths.refresh();
// final createdTheme = await _loadCreatedTheme();
// Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: {
// 'theme': createdTheme ?? _buildThemeItem(),
// });
final createdTheme = await _loadCreatedTheme();
await Get.toNamed(
Routes.WATCH_THEME_CUSTOM_PREVIEW,
arguments: {
'theme': createdTheme ?? _buildThemeItem(uploadedImageUrls),
},
);
} catch (_) {
AppToast.show('创建表盘失败,请重试');
} finally {
isSaving.value = false;
}
}
WatchThemeItem _buildThemeItem() {
WatchThemeItem _buildThemeItem(List<String> imageUrls) {
return WatchThemeItem(
themeName: customThemeName.value,
energeticDescription: customStatusNames[0],
energeticImage: customImagePaths[0],
energeticImage: imageUrls[0],
normalDescription: customStatusNames[1],
normalImage: customImagePaths[1],
normalImage: imageUrls[1],
slightStressfulDescription: customStatusNames[2],
slightStressfulImage: customImagePaths[2],
slightStressfulImage: imageUrls[2],
stressfulDescription: customStatusNames[3],
stressfulImage: customImagePaths[3],
stressfulImage: imageUrls[3],
);
}
... ...
... ... @@ -71,6 +71,10 @@ class CustomWatchThemePreviewController extends GetxController {
}
}
_changeFriend() {
//TODO: - bottomSheet 的方式展示出 SelectFriendView
}
Future<void> addWatchFace() async {
WearDeviceInfo? device;
try {
... ... @@ -89,7 +93,7 @@ class CustomWatchThemePreviewController extends GetxController {
await Get.dialog<void>(
WatchThemeSyncDialog(
faceAsset: R.assetsImagesWatchThemeCustomFacePreview,
themeImageUrl: themeItem.energeticImage,
onSync: _applyAndSyncWatchFace,
),
barrierDismissible: false,
... ...
... ... @@ -59,7 +59,7 @@ class WatchThemePreviewController extends GetxController {
await Get.dialog<void>(
WatchThemeSyncDialog(
faceAsset: R.assetsImagesWatchThemeFaceDefault,
themeImageUrl: themeItem.energeticImage,
onSync: _applyAndSyncWatchFace,
),
barrierDismissible: false,
... ... @@ -67,6 +67,10 @@ class WatchThemePreviewController extends GetxController {
);
}
_changeFriend() {
//TODO: - bottomSheet 的方式展示出 SelectFriendView
}
Future<bool> _applyAndSyncWatchFace() async {
final themeId = themeItem.id;
if (themeId == null) {
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
... ... @@ -64,10 +62,13 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> {
child: Column(
children: [
SizedBox(height: 12),
WatchFacePreview(
faceAsset:
R.assetsImagesWatchThemeCustomFaceCreate,
),
Obx(() {
return WatchFacePreview(
type: WatchFacePreviewType.singleMedium,
themeImageUrl:
controller.customImagePaths.first,
);
}),
SizedBox(height: 20),
_EditorCard(controller: controller),
SizedBox(height: 26),
... ...
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/widgets/dial_preview_card.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../controllers/custom_watch_theme_preview_controller.dart';
import '../widgets/status_preview_card.dart';
import '../widgets/watch_face_preview.dart';
import '../widgets/watch_theme_bottom_actions.dart';
import '../widgets/watch_theme_colors.dart';
... ... @@ -62,7 +62,7 @@ class CustomWatchThemePreviewView
'删除',
style: TextStyle(
color: const Color(0xFFFC4447),
fontSize: 14.dp,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
... ... @@ -71,18 +71,20 @@ class CustomWatchThemePreviewView
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.only(bottom: 96.dp),
padding: const EdgeInsets.only(bottom: 96),
child: Column(
children: [
WatchThemeHeader(
title: controller.themeItem.title,
faceAsset:
R.assetsImagesWatchThemeCustomFacePreview,
themeImageUrl: controller.themeItem.energeticImage,
),
SizedBox(height: 26.dp),
_CustomStatusPreviewCard(controller: controller),
SizedBox(height: 12.dp),
const _CustomDialPreviewCard(),
const SizedBox(height: 26),
StatusPreviewCard(themeItem: controller.themeItem),
const SizedBox(height: 12),
DialPreviewCard(
showFriend: true,
themeImageUrl: controller.themeItem.energeticImage,
)
],
),
),
... ... @@ -102,92 +104,3 @@ class CustomWatchThemePreviewView
);
}
}
class _CustomStatusPreviewCard extends StatelessWidget {
const _CustomStatusPreviewCard({required this.controller});
final CustomWatchThemePreviewController controller;
static final _assets = [
R.assetsImagesWatchThemeCustomStatusExcellent,
R.assetsImagesWatchThemeCustomStatusNormal,
R.assetsImagesWatchThemeCustomStatusStress,
R.assetsImagesWatchThemeCustomStatusOverload,
];
@override
Widget build(BuildContext context) {
final infoList = controller.themeItem.infoList;
return WatchThemeSectionCard(
padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WatchThemeSectionTitle('状态预览'),
SizedBox(height: 24.dp),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (var i = 0; i < 4; i++)
Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16.dp),
child: Image.asset(
_assets[i],
width: 60.dp,
height: 60.dp,
fit: BoxFit.cover,
),
),
SizedBox(height: 4.dp),
Text(
i < infoList.length ? infoList[i].title : '',
style: TextStyle(
color: defaultThemeTemplates[i].color,
fontSize: 12.dp,
),
),
],
),
],
),
],
),
);
}
}
class _CustomDialPreviewCard extends StatelessWidget {
const _CustomDialPreviewCard();
@override
Widget build(BuildContext context) {
return WatchThemeSectionCard(
padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WatchThemeSectionTitle('表盘预览'),
SizedBox(height: 16.dp),
Row(
children: [
WatchFacePreview(
width: 114,
height: 136,
faceAsset: R.assetsImagesWatchThemeCustomPreviewAlt,
),
SizedBox(width: 18.dp),
WatchFacePreview(
width: 114,
height: 136,
faceAsset: R.assetsImagesWatchThemeCustomFacePreview,
),
],
),
],
),
);
}
}
... ...
... ... @@ -55,7 +55,7 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> {
),
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 96),
child: Column(
children: [
... ... @@ -63,7 +63,9 @@ class WatchThemePreviewView extends GetView<WatchThemePreviewController> {
const SizedBox(height: 26),
StatusPreviewCard(themeItem: controller.themeItem),
const SizedBox(height: 12),
const DialPreviewCard(),
DialPreviewCard(
showFriend: true,
),
],
),
),
... ...
... ... @@ -55,7 +55,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
Expanded(
child: Obx(
() => SingleChildScrollView(
physics: const BouncingScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 24),
child: Column(
children: [
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
import 'watch_face_preview.dart';
import 'watch_theme_section_card.dart';
class DialPreviewCard extends StatelessWidget {
const DialPreviewCard({super.key});
bool showFriend;
final String? themeImageUrl;
final String? themeImageUrl2;
DialPreviewCard(
{super.key,
required this.showFriend,
this.themeImageUrl,
this.themeImageUrl2});
@override
Widget build(BuildContext context) {
return WatchThemeSectionCard(
padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 0, 27.dp),
padding: EdgeInsets.fromLTRB(20, 20, 0, 27),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WatchThemeSectionTitle('表盘预览'),
SizedBox(height: 16.dp),
SizedBox(height: 16),
SizedBox(
height: 136.dp,
height: 136,
child: ListView.separated(
padding: EdgeInsets.zero,
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == 0) {
return WatchFacePreview(
type: WatchFacePreviewType.surface,
themeImageUrl: themeImageUrl,
);
} else if (showFriend && index == 1) {
return WatchFacePreview(
type: WatchFacePreviewType.coupleSmall,
themeImageUrl: themeImageUrl,
themeImageUrl2: themeImageUrl2,
);
}
return WatchFacePreview(
width: 114,
height: 136,
faceAsset: index == 0
? R.assetsImagesWatchThemeFacePreviewAlt
: R.assetsImagesWatchThemeFaceDefault,
type: WatchFacePreviewType.singleSmall,
themeImageUrl: themeImageUrl,
);
},
separatorBuilder: (context, index) => SizedBox(width: 18.dp),
itemCount: 2,
separatorBuilder: (context, index) => SizedBox(width: 12),
itemCount: showFriend ? 3 : 2,
),
),
],
... ...
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'watch_theme_character_avatar.dart';
... ... @@ -12,12 +11,12 @@ class StatusPreviewCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WatchThemeSectionCard(
padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 17.dp),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 17),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WatchThemeSectionTitle('状态预览'),
SizedBox(height: 24.dp),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _previewItemView(),
... ... @@ -38,12 +37,12 @@ class StatusPreviewCard extends StatelessWidget {
assetPath: infoList[i].assetPath,
imageUrl: infoList[i].imgUrl,
),
SizedBox(height: 4.dp),
const SizedBox(height: 4),
Text(
i < infoList.length ? infoList[i].title : '',
style: TextStyle(
color: defaultThemeTemplates[i].color,
fontSize: 12.dp,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'watch_theme_colors.dart';
enum WatchFacePreviewType {
surface,
singleSmall,
singleMedium,
coupleSmall,
}
class WatchFacePreview extends StatelessWidget {
const WatchFacePreview({
super.key,
this.width = 134,
this.height = 160,
this.faceAsset,
this.faceFilePath,
this.type = WatchFacePreviewType.singleMedium,
this.themeImageUrl,
this.themeImageUrl2,
});
final double width;
final double height;
final String? faceAsset;
final String? faceFilePath;
final String? themeImageUrl;
final String? themeImageUrl2;
final WatchFacePreviewType type;
String get _backgroundAsset {
if (type == WatchFacePreviewType.surface) {
return 'assets/images/watch_theme/watch_theme_preview_surface.png';
}
if (type == WatchFacePreviewType.coupleSmall) {
return 'assets/images/watch_theme/watch_theme_preview_couple.png';
}
return 'assets/images/watch_theme/watch_theme_preview_single.png';
}
Size get _previewSize {
if (type == WatchFacePreviewType.singleMedium) {
return Size(134, 160);
}
return Size(120, 136);
}
Size get _themeSize {
if (type == WatchFacePreviewType.singleMedium) {
return Size(78, 78);
}
if (type == WatchFacePreviewType.singleSmall) {
return Size(55, 55);
}
if (type == WatchFacePreviewType.surface) {
return Size(44, 44);
}
return Size(55, 55);
}
@override
Widget build(BuildContext context) {
final w = width.dp;
final h = height.dp;
final inset = 6.dp;
final knobWidth = (width * 0.067).dp;
final knobHeight = (height * 0.156).dp;
final size = _previewSize;
return SizedBox(
width: w + knobWidth,
height: h,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 0,
top: 0,
child: Container(
width: w,
height: h,
decoration: BoxDecoration(
color: WatchThemeColors.watchShell,
borderRadius: BorderRadius.circular((width * 0.239).dp),
),
),
Image(
image: AssetImage(_backgroundAsset),
width: size.width,
height: size.height,
fit: BoxFit.fitWidth,
),
Positioned(
left: inset,
top: inset,
child: Container(
width: w - inset * 2,
height: h - inset * 2,
decoration: BoxDecoration(
color: WatchThemeColors.watchScreen,
borderRadius: BorderRadius.circular((width * 0.209).dp),
),
clipBehavior: Clip.antiAlias,
child: _buildFaceImage(),
),
),
Positioned(
left: w - 4.dp,
top: (height * 0.2375).dp,
child: Container(
width: knobWidth,
height: knobHeight,
decoration: BoxDecoration(
color: WatchThemeColors.watchShell,
borderRadius: BorderRadius.horizontal(
right: Radius.circular(5.dp),
),
),
_buildThemeImages(),
],
),
);
}
Widget _buildThemeImages() {
final themeSize = _themeSize;
if (type == WatchFacePreviewType.singleMedium) {
return Positioned(
top: 40,
width: themeSize.width,
height: themeSize.height,
child: _buildFaceImage(themeImageUrl,
'assets/images/watch_theme/official_default_green.png'),
);
}
if (type == WatchFacePreviewType.singleSmall) {
return Positioned(
top: 36,
width: themeSize.width,
height: themeSize.height,
child: _buildFaceImage(themeImageUrl,
'assets/images/watch_theme/official_default_green.png'),
);
}
if (type == WatchFacePreviewType.surface) {
return Positioned(
top: 90,
left: 24,
width: themeSize.width,
height: themeSize.height,
child: _buildFaceImage(themeImageUrl,
'assets/images/watch_theme/official_default_green.png'),
);
}
return Positioned(
top: 30,
child: Row(
children: [
SizedBox(
width: themeSize.width,
height: themeSize.height,
child: _buildFaceImage(themeImageUrl,
'assets/images/watch_theme/official_default_green.png'),
),
SizedBox(
width: 8,
),
Positioned(
left: w - 1.dp,
top: (height * 0.5125).dp,
child: Container(
width: 3.dp,
height: (height * 0.256).dp,
decoration: BoxDecoration(
color: WatchThemeColors.watchShell,
borderRadius: BorderRadius.horizontal(
right: Radius.circular(2.dp),
),
),
),
SizedBox(
width: themeSize.width,
height: themeSize.height,
child: _buildFaceImage(themeImageUrl2,
'assets/images/watch_theme/official_default_blue.png'),
),
],
),
);
}
Widget _buildFaceImage() {
if (faceFilePath != null && faceFilePath!.isNotEmpty) {
return Image.file(File(faceFilePath!), fit: BoxFit.cover);
Widget _buildFaceImage(String? themeUrl, String placeholder) {
if (themeUrl != null && themeUrl.isNotEmpty) {
if (themeUrl.isURL) {
return CachedNetworkImage(imageUrl: themeUrl);
}
return Image.file(File(themeUrl), fit: BoxFit.cover);
}
return Image.asset(
faceAsset ?? R.assetsImagesWatchThemeFaceDefault,
placeholder,
fit: BoxFit.cover,
);
}
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'watch_face_preview.dart';
... ... @@ -8,31 +7,29 @@ class WatchThemeHeader extends StatelessWidget {
const WatchThemeHeader({
super.key,
this.title = '默认主题',
this.faceAsset,
this.faceFilePath,
this.themeImageUrl,
});
final String title;
final String? faceAsset;
final String? faceFilePath;
final String? themeImageUrl;
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(height: 12.dp),
SizedBox(height: 12),
Center(
child: WatchFacePreview(
faceAsset: faceAsset,
faceFilePath: faceFilePath,
type: WatchFacePreviewType.singleMedium,
themeImageUrl: themeImageUrl,
),
),
SizedBox(height: 12.dp),
SizedBox(height: 12),
Text(
title,
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 20.dp,
fontSize: 20,
fontWeight: FontWeight.w600,
height: 1.25,
),
... ...
... ... @@ -9,13 +9,11 @@ class WatchThemeSyncDialog extends StatefulWidget {
const WatchThemeSyncDialog({
super.key,
required this.onSync,
this.faceAsset,
this.faceFilePath,
this.themeImageUrl,
});
final Future<bool> Function() onSync;
final String? faceAsset;
final String? faceFilePath;
final String? themeImageUrl;
@override
State<WatchThemeSyncDialog> createState() => _WatchThemeSyncDialogState();
... ... @@ -76,10 +74,8 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> {
right: 0,
child: Center(
child: WatchFacePreview(
width: 134,
height: 160,
faceAsset: widget.faceAsset,
faceFilePath: widget.faceFilePath,
type: WatchFacePreviewType.singleMedium,
themeImageUrl: widget.themeImageUrl,
),
),
),
... ...
import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart';
import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
... ... @@ -65,6 +66,8 @@ abstract final class AppRoutes {
abstract final class AppPages {
static const initialRoute = AppRoutes.splash;
static final environmentConfig = Get.find<AppEnvironmentConfig>();
static final routes = [
GetPage(
name: AppRoutes.splash,
... ... @@ -81,7 +84,7 @@ abstract final class AppPages {
page: () => const PhoneLoginView(),
binding: LoginBinding(),
),
if (kDebugMode)
if (environmentConfig.isDebug)
GetPage(
name: AppRoutes.debugEnvironment,
page: () => const DebugEnvironmentView(),
... ...
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import '../constants/app_const.dart';
... ... @@ -10,11 +12,13 @@ class AppEnvironmentConfig {
AppEnvironmentConfig(this._storage);
final LocalStorage _storage;
final PlatformHostApi _platformHostApi = PlatformHostApi();
final Rx<AppEnvironment> environment = AppEnvironment.release.obs;
Future<AppEnvironmentConfig> init() async {
environment.value = await _storage.readEnvironment();
_isNativeDebug = await _platformHostApi.isDebugEnvoriment();
return this;
}
... ... @@ -23,6 +27,9 @@ class AppEnvironmentConfig {
await _storage.writeEnvironment(value);
}
bool _isNativeDebug = false;
bool get isDebug => kDebugMode || _isNativeDebug;
String get serverBaseUrl => resolveServerUrl(AppConst.serverBaseUrl);
String resolveServerUrl(String url) {
... ...
... ... @@ -30,6 +30,11 @@ bool _deepEquals(Object? a, Object? b) {
}
enum HResourceType {
image,
video,
}
class AppleSignInModel {
AppleSignInModel({
required this.userId,
... ... @@ -305,17 +310,20 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is AppleSignInModel) {
} else if (value is HResourceType) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is AppleSignInModel) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is WatchAppOtherInfo) {
buffer.putUint8(130);
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is AppleProductInfo) {
buffer.putUint8(131);
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is AppleProductPaymentResult) {
buffer.putUint8(132);
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
... ... @@ -326,12 +334,15 @@ class _PigeonCodec extends StandardMessageCodec {
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
return AppleSignInModel.decode(readValue(buffer)!);
final int? value = readValue(buffer) as int?;
return value == null ? null : HResourceType.values[value];
case 130:
return WatchAppOtherInfo.decode(readValue(buffer)!);
return AppleSignInModel.decode(readValue(buffer)!);
case 131:
return AppleProductInfo.decode(readValue(buffer)!);
return WatchAppOtherInfo.decode(readValue(buffer)!);
case 132:
return AppleProductInfo.decode(readValue(buffer)!);
case 133:
return AppleProductPaymentResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
... ... @@ -382,6 +393,35 @@ class PlatformHostApi {
}
}
/// 是否是测试环境
Future<bool> isDebugEnvoriment() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// 更新用户信息, 有登录态后调用
/// jsonString: UserPreferences的序列化string
/// baseUrl: 请求地址, https://api.doublefeel.cn
... ... @@ -456,7 +496,7 @@ class PlatformHostApi {
}
}
/// 刷新watch app 和 表盘的所有数据:
/// 刷新watch app 和 表盘的所有数据:
/// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
Future<void> refreshWatchAppAndWidgets() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.refreshWatchAppAndWidgets$pigeonVar_messageChannelSuffix';
... ... @@ -481,6 +521,35 @@ class PlatformHostApi {
}
}
/// 请求评分弹窗
Future<bool> requestAppReview() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppReview$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// 请求苹果登录
Future<AppleSignInModel?> requestAppleSignIn() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestAppleSignIn$pigeonVar_messageChannelSuffix';
... ... @@ -583,4 +652,29 @@ class PlatformHostApi {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// 上传文件到云端
/// 注意catch flutter error
Future<String?> uploadFile(String filePath, HResourceType resourceType) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.uploadFile$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[filePath, resourceType]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return (pigeonVar_replyList[0] as String?);
}
}
}
... ...
... ... @@ -280,4 +280,33 @@ class WearEngineHostApi {
return (pigeonVar_replyList[0] as String?);
}
}
/// 是否有已安装的表盘
Future<bool> hasInstalledWatchSurface() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasInstalledWatchSurface$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
}
... ...
... ... @@ -40,36 +40,6 @@ class R {
static final String assetsImagesMyAvatarDefault =
'assets/images/my/avatar_default.png';
// watch theme
static final String assetsImagesWatchThemeFaceDefault =
'assets/images/watch_theme/watch_face_default.png';
static final String assetsImagesWatchThemeFacePreviewAlt =
'assets/images/watch_theme/watch_face_preview_alt.png';
static final String assetsImagesWatchThemeCustomFaceCreate =
'assets/images/watch_theme/custom_watch_face_create.png';
static final String assetsImagesWatchThemeCustomFacePreview =
'assets/images/watch_theme/custom_watch_face_preview.png';
static final String assetsImagesWatchThemeCustomPreviewAlt =
'assets/images/watch_theme/custom_preview_alt.png';
static final String assetsImagesWatchThemeCustomStatusExcellent =
'assets/images/watch_theme/custom_status_excellent.png';
static final String assetsImagesWatchThemeCustomStatusNormal =
'assets/images/watch_theme/custom_status_normal.png';
static final String assetsImagesWatchThemeCustomStatusStress =
'assets/images/watch_theme/custom_status_stress.png';
static final String assetsImagesWatchThemeCustomStatusOverload =
'assets/images/watch_theme/custom_status_overload.png';
static final String assetsImagesWatchThemeOfficialDogWhite =
'assets/images/watch_theme/official_dog_white.png';
static final String assetsImagesWatchThemeOfficialRabbitPink =
'assets/images/watch_theme/official_rabbit_pink.png';
static final String assetsImagesWatchThemeOfficialCatOrange =
'assets/images/watch_theme/official_cat_orange.png';
static final String assetsImagesWatchThemeOfficialElephantBlue =
'assets/images/watch_theme/official_elephant_blue.png';
static final String assetsImagesWatchThemeCustomCatDog =
'assets/images/watch_theme/custom_cat_dog.png';
// real-time stress
static final String assetsImagesRealtimeStressAttentionIcon =
'assets/images/common/ic_health_stress_attention.webp';
... ...
... ... @@ -116,6 +116,9 @@ abstract class PlatformHostApi {
/// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
String getFullUserAgent();
/// 是否是测试环境
bool isDebugEnvoriment();
/// 更新用户信息, 有登录态后调用
/// jsonString: UserPreferences的序列化string
/// baseUrl: 请求地址, https://api.doublefeel.cn
... ... @@ -131,6 +134,10 @@ abstract class PlatformHostApi {
/// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
void refreshWatchAppAndWidgets();
/// 请求评分弹窗
@async
bool requestAppReview();
/// 请求苹果登录
@async
AppleSignInModel? requestAppleSignIn();
... ...
... ... @@ -40,9 +40,12 @@ abstract class WearEngineHostApi {
bool sendWatchSyncPayload(String jsonPayload);
/// Opens the system photo picker and returns a local PNG file path after
/// removing the image background on the host platform.
@async
String? removeBackground(String originImagePath);
/// 是否有已安装的表盘
@async
bool hasInstalledWatchSurface();
}
... ...
... ... @@ -430,13 +430,13 @@ packages:
source: hosted
version: "6.1.0"
image_cropper_platform_interface:
dependency: transitive
dependency: "direct overridden"
description:
name: image_cropper_platform_interface
sha256: "2d8db8f4b638e448fa89a1e77cd8f053b4547472bd3ae073169e86626d03afef"
sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
url: "https://pub.dev"
source: hosted
version: "7.2.0"
version: "7.1.0"
image_picker:
dependency: "direct main"
description:
... ... @@ -647,7 +647,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct overridden"
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
... ...
... ... @@ -51,6 +51,7 @@ dependencies:
video_thumbnail: ^0.5.3
table_calendar: ^3.1.3
fluttertoast: ^8.2.2
path_provider: ^2.1.5
flutter_localizations:
sdk: flutter
intl: any
... ... @@ -60,6 +61,7 @@ dependencies:
# 强制使用 hosted 版本统一来源。
dependency_overrides:
path_provider: ^2.1.5
image_cropper_platform_interface: 7.1.0
dev_dependencies:
flutter_test:
... ...