|
|
|
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],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
...
|
...
|
|