Commit 545418f38b7c00c465bf3a2fe89d453ab0eba7c6

Authored by 权海
1 parent c8044b54

feat(ui):修改一些代码

... ... @@ -308,7 +308,10 @@ interface HealthKitHostApi {
fun cancelHealthAppAuthorization(): Boolean
/** Runs native health read and server upload pipeline. */
fun performHealthUpload(callback: (Result<HealthUploadResult>) -> Unit)
/** Opens Huawei Health client authorization UI. Returns whether user granted. */
/**
* 获取当前已上传的健康数据点,主要用于调试
* Opens Huawei Health client authorization UI. Returns whether user granted.
*/
fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
fun fetchHrvData(startTime: Long, endTime: Long, callback: (Result<List<HealthUploadDataPoint>>) -> Unit)
... ...
... ... @@ -118,6 +118,61 @@ data class WearDeviceInfo (
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class WatchTransformTheme (
val themeId: Long,
val themeName: String,
val energeticDescription: String,
val energeticImage: ByteArray? = null,
val normalDescription: String,
val normalImage: ByteArray? = null,
val slightStressfulDescription: String,
val slightStressfulImage: ByteArray? = null,
val stressfulDescription: String,
val stressfulImage: ByteArray? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): WatchTransformTheme {
val themeId = pigeonVar_list[0] as Long
val themeName = pigeonVar_list[1] as String
val energeticDescription = pigeonVar_list[2] as String
val energeticImage = pigeonVar_list[3] as ByteArray?
val normalDescription = pigeonVar_list[4] as String
val normalImage = pigeonVar_list[5] as ByteArray?
val slightStressfulDescription = pigeonVar_list[6] as String
val slightStressfulImage = pigeonVar_list[7] as ByteArray?
val stressfulDescription = pigeonVar_list[8] as String
val stressfulImage = pigeonVar_list[9] as ByteArray?
return WatchTransformTheme(themeId, themeName, energeticDescription, energeticImage, normalDescription, normalImage, slightStressfulDescription, slightStressfulImage, stressfulDescription, stressfulImage)
}
}
fun toList(): List<Any?> {
return listOf(
themeId,
themeName,
energeticDescription,
energeticImage,
normalDescription,
normalImage,
slightStressfulDescription,
slightStressfulImage,
stressfulDescription,
stressfulImage,
)
}
override fun equals(other: Any?): Boolean {
if (other !is WatchTransformTheme) {
return false
}
if (this === other) {
return true
}
return WearEngineApiPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
private open class WearEngineApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
... ... @@ -126,6 +181,11 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() {
WearDeviceInfo.fromList(it)
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
WatchTransformTheme.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
... ... @@ -135,6 +195,10 @@ private open class WearEngineApiPigeonCodec : StandardMessageCodec() {
stream.write(129)
writeValue(stream, value.toList())
}
is WatchTransformTheme -> {
stream.write(130)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
... ... @@ -149,7 +213,8 @@ interface WearEngineHostApi {
fun sendTextMessage(message: String): Boolean
/** 添加表盘 */
fun addWatchSurface(callback: (Result<Boolean>) -> Unit)
fun sendWatchSyncPayload(jsonPayload: String, callback: (Result<Boolean>) -> Unit)
/** 添加或者删除主题 */
fun syncWatchTheme(theme: WatchTransformTheme?, callback: (Result<Boolean>) -> Unit)
/**
* Opens the system photo picker and returns a local PNG file path after
* removing the image background on the host platform.
... ... @@ -251,12 +316,12 @@ interface WearEngineHostApi {
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$separatedMessageChannelSuffix", codec)
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val jsonPayloadArg = args[0] as String
api.sendWatchSyncPayload(jsonPayloadArg) { result: Result<Boolean> ->
val themeArg = args[0] as WatchTransformTheme?
api.syncWatchTheme(themeArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(WearEngineApiPigeonUtils.wrapError(error))
... ...
... ... @@ -333,6 +333,7 @@ protocol HealthKitHostApi {
func cancelHealthAppAuthorization() throws -> Bool
/// Runs native health read and server upload pipeline.
func performHealthUpload(completion: @escaping (Result<HealthUploadResult, Error>) -> Void)
/// 获取当前已上传的健康数据点,主要用于调试
/// Opens Huawei Health client authorization UI. Returns whether user granted.
func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void)
func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)
... ... @@ -403,6 +404,7 @@ class HealthKitHostApiSetup {
} else {
performHealthUploadChannel.setMessageHandler(nil)
}
/// 获取当前已上传的健康数据点,主要用于调试
/// Opens Huawei Health client authorization UI. Returns whether user granted.
let checkHealthAppAuthorizationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
... ...
... ... @@ -153,11 +153,74 @@ struct WearDeviceInfo: Hashable {
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct WatchTransformTheme: Hashable {
var themeId: Int64
var themeName: String
var energeticDescription: String
var energeticImage: FlutterStandardTypedData? = nil
var normalDescription: String
var normalImage: FlutterStandardTypedData? = nil
var slightStressfulDescription: String
var slightStressfulImage: FlutterStandardTypedData? = nil
var stressfulDescription: String
var stressfulImage: FlutterStandardTypedData? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> WatchTransformTheme? {
let themeId = pigeonVar_list[0] as! Int64
let themeName = pigeonVar_list[1] as! String
let energeticDescription = pigeonVar_list[2] as! String
let energeticImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[3])
let normalDescription = pigeonVar_list[4] as! String
let normalImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[5])
let slightStressfulDescription = pigeonVar_list[6] as! String
let slightStressfulImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[7])
let stressfulDescription = pigeonVar_list[8] as! String
let stressfulImage: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[9])
return WatchTransformTheme(
themeId: themeId,
themeName: themeName,
energeticDescription: energeticDescription,
energeticImage: energeticImage,
normalDescription: normalDescription,
normalImage: normalImage,
slightStressfulDescription: slightStressfulDescription,
slightStressfulImage: slightStressfulImage,
stressfulDescription: stressfulDescription,
stressfulImage: stressfulImage
)
}
func toList() -> [Any?] {
return [
themeId,
themeName,
energeticDescription,
energeticImage,
normalDescription,
normalImage,
slightStressfulDescription,
slightStressfulImage,
stressfulDescription,
stressfulImage,
]
}
static func == (lhs: WatchTransformTheme, rhs: WatchTransformTheme) -> Bool {
return deepEqualsWearEngineApi(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashWearEngineApi(value: toList(), hasher: &hasher)
}
}
private class WearEngineApiPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
return WearDeviceInfo.fromList(self.readValue() as! [Any?])
case 130:
return WatchTransformTheme.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
... ... @@ -169,6 +232,9 @@ private class WearEngineApiPigeonCodecWriter: FlutterStandardWriter {
if let value = value as? WearDeviceInfo {
super.writeByte(129)
super.writeValue(value.toList())
} else if let value = value as? WatchTransformTheme {
super.writeByte(130)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
... ... @@ -198,7 +264,8 @@ protocol WearEngineHostApi {
func sendTextMessage(message: String) throws -> Bool
/// 添加表盘
func addWatchSurface(completion: @escaping (Result<Bool, Error>) -> Void)
func sendWatchSyncPayload(jsonPayload: String, completion: @escaping (Result<Bool, Error>) -> Void)
/// 添加或者删除主题
func syncWatchTheme(theme: WatchTransformTheme?, completion: @escaping (Result<Bool, Error>) -> Void)
/// 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)
... ... @@ -284,12 +351,13 @@ class WearEngineHostApiSetup {
} else {
addWatchSurfaceChannel.setMessageHandler(nil)
}
let sendWatchSyncPayloadChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
/// 添加或者删除主题
let syncWatchThemeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
sendWatchSyncPayloadChannel.setMessageHandler { message, reply in
syncWatchThemeChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let jsonPayloadArg = args[0] as! String
api.sendWatchSyncPayload(jsonPayload: jsonPayloadArg) { result in
let themeArg: WatchTransformTheme? = nilOrValue(args[0])
api.syncWatchTheme(theme: themeArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
... ... @@ -299,7 +367,7 @@ class WearEngineHostApiSetup {
}
}
} else {
sendWatchSyncPayloadChannel.setMessageHandler(nil)
syncWatchThemeChannel.setMessageHandler(nil)
}
/// Opens the system photo picker and returns a local PNG file path after
/// removing the image background on the host platform.
... ...
... ... @@ -41,10 +41,20 @@ final class WearEngineHostApiImpl: WearEngineHostApi {
watchService.sendTextMessage(message)
}
func sendWatchSyncPayload(jsonPayload: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
let success = WatchConnectivityService.shared.sendWatchThemeChangedMessage(json: jsonPayload)
completion(.success(success))
}
func syncWatchTheme(theme: WatchTransformTheme?, completion: @escaping (Result<Bool, any Error>) -> Void) {
guard let theme else{
//TODO: - 删除主题
completion(.success(true))
return
}
let success = WatchThemeStore.shared.saveTheme(theme)
guard success else {
completion(.success(false))
return
}
let notified = watchService.sendWatchThemeChangedMessage()
completion(.success(notified))
}
func removeBackground(originImagePath: String, completion: @escaping (Result<String?, any Error>) -> Void) {
// 抠图
... ...
... ... @@ -53,6 +53,7 @@ final class WatchConnectivityService: NSObject {
return WearDeviceInfo(
deviceId: nil,
deviceName: "Apple Watch",
isPaired: isPaired,
isReachable: isReachable,
isWatchAppInstalled: isWatchAppInstalled
)
... ... @@ -111,7 +112,7 @@ final class WatchConnectivityService: NSObject {
return true
}
func sendWatchThemeChangedMessage(json: String) -> Bool{
func sendWatchThemeChangedMessage(json: String? = nil) -> Bool{
sendCommandMessage(AppGroupMessageKey.reloadTheme, json: json)
}
}
... ...
import Foundation
/// Shared Watch theme persistence.
/// The Watch app and Widget extension read this exact JSON string from App Group.
/// The Watch app and Widget extension read this exact JSON data from App Group.
final class WatchThemeStore {
static let shared = WatchThemeStore()
... ... @@ -9,13 +9,95 @@ final class WatchThemeStore {
@discardableResult
func saveThemeJSONString(_ json: String) -> Bool {
guard json.data(using: .utf8) != nil else { return false }
AppGroupConstants.defaults?.set(json, forKey: AppGroupConstants.Key.myWatchTheme)
guard let data = json.data(using: .utf8) else { return false }
AppGroupConstants.defaults?.set(data, forKey: AppGroupConstants.Key.myWatchTheme)
return true
}
@discardableResult
func saveTheme(_ theme: WatchTransformTheme) -> Bool {
guard let defaults = AppGroupConstants.defaults else { return false }
let imageStore = SyncedWatchThemeImageStore(themeId: theme.themeId)
let model = SyncedWatchThemeModel(
id: Int(theme.themeId),
themeName: theme.themeName,
isOfficial: 0,
energeticTitle: theme.energeticDescription,
energeticImageURL: imageStore.save(
theme.energeticImage?.data,
status: "energetic",
defaults: defaults
),
normalTitle: theme.normalDescription,
normalImageURL: imageStore.save(
theme.normalImage?.data,
status: "normal",
defaults: defaults
),
slightStressTitle: theme.slightStressfulDescription,
slightStressImageURL: imageStore.save(
theme.slightStressfulImage?.data,
status: "slight_stressful",
defaults: defaults
),
stressfulTitle: theme.stressfulDescription,
stressfulImageURL: imageStore.save(
theme.stressfulImage?.data,
status: "stressful",
defaults: defaults
)
)
do {
let data = try JSONEncoder().encode(model)
defaults.set(data, forKey: AppGroupConstants.Key.myWatchTheme)
defaults.synchronize()
return true
} catch {
return false
}
}
func clearTheme() {
AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.myWatchTheme)
}
}
private struct SyncedWatchThemeImageStore {
let themeId: Int64
func save(_ data: Data?, status: String, defaults: UserDefaults) -> String? {
guard let data else { return nil }
let key = "watch_theme_\(themeId)_\(status)"
defaults.set(data, forKey: key)
return key
}
}
private struct SyncedWatchThemeModel: Codable {
let id: Int?
let themeName: String?
let isOfficial: Int?
let energeticTitle: String?
let energeticImageURL: String?
let normalTitle: String?
let normalImageURL: String?
let slightStressTitle: String?
let slightStressImageURL: String?
let stressfulTitle: String?
let stressfulImageURL: String?
enum CodingKeys: String, CodingKey {
case id
case themeName = "theme_name"
case isOfficial = "is_official"
case energeticTitle = "energetic_description"
case energeticImageURL = "energetic_image"
case normalTitle = "normal_description"
case normalImageURL = "normal_image"
case slightStressTitle = "slight_stressful_description"
case slightStressImageURL = "slight_stressful_image"
case stressfulTitle = "stressful_description"
case stressfulImageURL = "stressful_image"
}
}
... ...
import 'dart:async';
import 'dart:convert';
import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/services/watch_theme_sync_payload_builder.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
... ... @@ -12,6 +12,7 @@ import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:logger/logger.dart';
import '../../friends/models/friend_health_data.dart';
import '../../friends/views/select_friend_view.dart';
... ... @@ -24,6 +25,8 @@ class CustomWatchThemePreviewController extends GetxController {
CustomWatchThemePreviewController(this._themeApi);
final ThemeApi _themeApi;
final WatchThemeSyncPayloadBuilder _syncPayloadBuilder =
const WatchThemeSyncPayloadBuilder();
final FriendsRepository _repository =
FriendsRepositoryImpl(Get.find<FriendApi>());
... ... @@ -120,6 +123,7 @@ class CustomWatchThemePreviewController extends GetxController {
AppToast.show(l10n.watchThemeDeleteFailed);
return;
}
await WearEngineHostApi().syncWatchTheme(null);
AppToast.show(l10n.watchThemeDeleted);
Get.back();
}
... ... @@ -160,10 +164,8 @@ class CustomWatchThemePreviewController extends GetxController {
}
try {
await WearEngineHostApi().addWatchSurface();
await PlatformHostApi().nativeHandleUrl("itms-watchs://");
} catch (e) {
AppToast.show(e.toString());
}
} catch (e) {}
await PlatformHostApi().nativeHandleUrl("itms-watchs://");
}
Future<void> tryApplyAndSyncWatchFace() async {
... ... @@ -201,20 +203,23 @@ class CustomWatchThemePreviewController extends GetxController {
}
isApplying.value = true;
final result = await _themeApi.applyTheme(themeId);
if (result is! AppSuccess<void>) {
isApplying.value = false;
AppToast.show(l10n.watchThemeApplyFailed);
return false;
}
selectedThemeItem.value = themeItem;
var syncSuccess = true;
try {
syncSuccess = await WearEngineHostApi().sendWatchSyncPayload(
jsonEncode(themeItem.toJson()),
);
final payload = await _syncPayloadBuilder.build(themeItem);
final success = await WearEngineHostApi().syncWatchTheme(payload);
if (success) {
final result = await _themeApi.applyTheme(themeId);
if (result is! AppSuccess<void>) {
isApplying.value = false;
AppToast.show(l10n.watchThemeApplyFailed);
return false;
}
syncSuccess = success;
selectedThemeItem.value = themeItem;
return false;
}
syncSuccess = success;
} catch (_) {
// The active theme is already saved on the server.
syncSuccess = false;
... ...
import 'dart:async';
import 'dart:convert';
import 'package:doublefeel_flutter/app/modules/friends/data/friends_repository.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_device_extension.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/services/watch_theme_sync_payload_builder.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
... ... @@ -23,6 +23,8 @@ class WatchThemePreviewController extends GetxController {
WatchThemePreviewController(this._themeApi);
final ThemeApi _themeApi;
final WatchThemeSyncPayloadBuilder _syncPayloadBuilder =
const WatchThemeSyncPayloadBuilder();
final FriendsRepository _repository =
FriendsRepositoryImpl(Get.find<FriendApi>());
... ... @@ -132,10 +134,10 @@ class WatchThemePreviewController extends GetxController {
try {
await WearEngineHostApi().addWatchSurface();
await PlatformHostApi().nativeHandleUrl("itms-watchs://");
} catch (e) {
AppToast.show(e.toString());
}
await PlatformHostApi().nativeHandleUrl("itms-watchs://");
}
Future<void> tryApplyAndSyncWatchFace() async {
... ... @@ -173,20 +175,23 @@ class WatchThemePreviewController extends GetxController {
}
isApplying.value = true;
final result = await _themeApi.applyTheme(themeId);
if (result is! AppSuccess<void>) {
isApplying.value = false;
AppToast.show(l10n.watchThemeApplyFailed);
return false;
}
selectedThemeItem.value = themeItem;
var syncSuccess = true;
try {
syncSuccess = await WearEngineHostApi().sendWatchSyncPayload(
jsonEncode(themeItem.toJson()),
);
final payload = await _syncPayloadBuilder.build(themeItem);
final success = await WearEngineHostApi().syncWatchTheme(payload);
if (success) {
final result = await _themeApi.applyTheme(themeId);
if (result is! AppSuccess<void>) {
isApplying.value = false;
AppToast.show(l10n.watchThemeApplyFailed);
return false;
}
syncSuccess = success;
selectedThemeItem.value = themeItem;
return false;
}
syncSuccess = success;
} catch (_) {
// The active theme is already saved on the server.
syncSuccess = false;
... ...
import 'dart:io';
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class WatchThemeSyncPayloadBuilder {
const WatchThemeSyncPayloadBuilder();
Future<WatchTransformTheme> build(WatchThemeItem item) async {
return WatchTransformTheme(
themeId: item.id ?? 0,
themeName: item.themeName,
energeticDescription: item.energeticDescription,
energeticImage: await _loadImageBytes(item.energeticImage),
normalDescription: item.normalDescription,
normalImage: await _loadImageBytes(item.normalImage),
slightStressfulDescription: item.slightStressfulDescription,
slightStressfulImage: await _loadImageBytes(item.slightStressfulImage),
stressfulDescription: item.stressfulDescription,
stressfulImage: await _loadImageBytes(item.stressfulImage),
);
}
Future<Uint8List?> _loadImageBytes(String? imagePath) async {
final value = imagePath?.trim();
if (value == null || value.isEmpty) return null;
final uri = Uri.tryParse(value);
if (uri != null && (uri.scheme == 'http' || uri.scheme == 'https')) {
return _downloadImage(uri);
}
final file = File(value);
if (await file.exists()) {
return file.readAsBytes();
}
try {
final data = await rootBundle.load(value);
return data.buffer.asUint8List();
} catch (_) {
return null;
}
}
Future<Uint8List?> _downloadImage(Uri uri) async {
final client = HttpClient();
try {
final request = await client.getUrl(uri);
final response = await request.close();
if (response.statusCode < 200 || response.statusCode >= 300) {
return null;
}
return consolidateHttpClientResponseBytes(response);
} catch (_) {
return null;
} finally {
client.close(force: true);
}
}
}
... ...
... ... @@ -55,6 +55,7 @@ class OfficialThemeGrid extends StatelessWidget {
size: 88,
child: WatchThemeCharacterAvatar(
size: 88,
padding: 8,
assetPath: theme.infoList.firstOrNull?.assetPath,
imageUrl: theme.infoList.firstOrNull?.imgUrl,
),
... ...
... ... @@ -4,12 +4,14 @@ class WatchThemeCharacterAvatar extends StatelessWidget {
const WatchThemeCharacterAvatar({
super.key,
required this.size,
this.padding = 0,
this.assetPath,
this.imageUrl,
this.empty = false,
});
final double size;
final double padding;
final String? assetPath;
final String? imageUrl;
final bool empty;
... ... @@ -18,24 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget {
Widget build(BuildContext context) {
final avatarSize = size;
return SizedBox(
width: avatarSize,
height: avatarSize,
width: avatarSize,
height: avatarSize,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
width: avatarSize,
width: avatarSize,
height: avatarSize,
alignment: Alignment.center,
child: empty || (assetPath == null && imageUrl == null)
? const SizedBox.shrink()
: _buildImage(avatarSize),
),
)
);
alignment: Alignment.center,
child: empty || (assetPath == null && imageUrl == null)
? const SizedBox.shrink()
: _buildImage(avatarSize),
),
));
}
Widget _buildImage(double avatarSize) {
final width = avatarSize;
final width = avatarSize - padding * 2;
if (imageUrl != null && imageUrl!.isNotEmpty) {
return Image.network(
imageUrl!,
... ...
... ... @@ -28,6 +28,9 @@ class _WatchThemeSyncDialogState extends State<WatchThemeSyncDialog> {
if (_state == _WatchThemeSyncState.syncing) return;
setState(() => _state = _WatchThemeSyncState.syncing);
final success = await widget.onSync();
if (success) {
await Future<void>.delayed(const Duration(seconds: 1));
}
if (!mounted) return;
setState(() {
_success = success;
... ...
... ... @@ -48,46 +48,4 @@ class WearEngineService {
return result is AppSuccess;
}
Future<bool> syncWatchUserPayload({required bool isOther}) async {
if (!isAndroid) {
return false;
}
final device = await _wearEngineHost.checkConnectedDevice();
if (device == null) {
return false;
}
final today = await _healthApi.getTodayData(isOther: isOther);
final prefs = _userPrefs.preferences.value;
final me = prefs.meUserInfo;
final partner = prefs.partnerUserInfo;
int hrvValue = 0;
int heartRate = 0;
int steps = 0;
if (today case AppSuccess(data: final todayData)) {
if (todayData.hrvDataList?.isNotEmpty == true) {
hrvValue = todayData.hrvDataList!.first.value?.toInt() ?? 0;
}
heartRate = todayData.recentData?.heartRate ?? 0;
steps = todayData.recentData?.steps ?? 0;
}
final payload = jsonEncode({
'dataType': 1,
'isLogin': _userPrefs.accessToken.isNotEmpty,
'isBound': (partner?.pairId ?? 0) > 0,
'isVip': prefs.vipInfo?.isVip ?? false,
'meCharacter': me?.persona ?? UserCharacter.cat.id,
'meHrv': hrvValue,
'meHeartRate': heartRate,
'meSteps': steps,
'taCharacter': partner?.persona ?? UserCharacter.cat.id,
});
final ok = await _wearEngineHost.sendWatchSyncPayload(payload);
AppLogger.i('WearEngineService.syncWatchUserPayload ok=$ok');
return ok;
}
}
... ...
... ... @@ -421,6 +421,7 @@ class HealthKitHostApi {
}
}
/// 获取当前已上传的健康数据点,主要用于调试
/// Opens Huawei Health client authorization UI. Returns whether user granted.
Future<HealthAuthorization> checkHealthAppAuthorization() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix';
... ...
... ... @@ -91,6 +91,92 @@ class WearDeviceInfo {
;
}
class WatchTransformTheme {
WatchTransformTheme({
required this.themeId,
required this.themeName,
required this.energeticDescription,
this.energeticImage,
required this.normalDescription,
this.normalImage,
required this.slightStressfulDescription,
this.slightStressfulImage,
required this.stressfulDescription,
this.stressfulImage,
});
int themeId;
String themeName;
String energeticDescription;
Uint8List? energeticImage;
String normalDescription;
Uint8List? normalImage;
String slightStressfulDescription;
Uint8List? slightStressfulImage;
String stressfulDescription;
Uint8List? stressfulImage;
List<Object?> _toList() {
return <Object?>[
themeId,
themeName,
energeticDescription,
energeticImage,
normalDescription,
normalImage,
slightStressfulDescription,
slightStressfulImage,
stressfulDescription,
stressfulImage,
];
}
Object encode() {
return _toList(); }
static WatchTransformTheme decode(Object result) {
result as List<Object?>;
return WatchTransformTheme(
themeId: result[0]! as int,
themeName: result[1]! as String,
energeticDescription: result[2]! as String,
energeticImage: result[3] as Uint8List?,
normalDescription: result[4]! as String,
normalImage: result[5] as Uint8List?,
slightStressfulDescription: result[6]! as String,
slightStressfulImage: result[7] as Uint8List?,
stressfulDescription: result[8]! as String,
stressfulImage: result[9] as Uint8List?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! WatchTransformTheme || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
... ... @@ -102,6 +188,9 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is WearDeviceInfo) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is WatchTransformTheme) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
... ... @@ -112,6 +201,8 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) {
case 129:
return WearDeviceInfo.decode(readValue(buffer)!);
case 130:
return WatchTransformTheme.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
... ... @@ -267,14 +358,15 @@ class WearEngineHostApi {
}
}
Future<bool> sendWatchSyncPayload(String jsonPayload) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix';
/// 添加或者删除主题
Future<bool> syncWatchTheme(WatchTransformTheme? theme) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.syncWatchTheme$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[jsonPayload]);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[theme]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
... ...
... ... @@ -83,6 +83,9 @@ abstract class HealthKitHostApi {
@async
HealthUploadResult performHealthUpload();
/// 获取当前已上传的健康数据点,主要用于调试
// List<HealthUploadDataPoint> getDebugCurrentUploadedData();
// @required
/// Opens Huawei Health client authorization UI. Returns whether user granted.
... ...
... ... @@ -16,6 +16,32 @@ class WearDeviceInfo {
bool? isWatchAppInstalled;
}
class WatchTransformTheme {
WatchTransformTheme({
required this.themeId,
required this.themeName,
required this.energeticDescription,
this.energeticImage,
required this.normalDescription,
this.normalImage,
required this.slightStressfulDescription,
this.slightStressfulImage,
required this.stressfulDescription,
this.stressfulImage,
});
int themeId;
final String themeName;
final String energeticDescription;
final Uint8List? energeticImage;
final String normalDescription;
final Uint8List? normalImage;
final String slightStressfulDescription;
final Uint8List? slightStressfulImage;
final String stressfulDescription;
final Uint8List? stressfulImage;
}
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/wear_engine_api.g.dart',
... ... @@ -47,8 +73,9 @@ abstract class WearEngineHostApi {
@async
bool addWatchSurface();
/// 添加或者删除主题
@async
bool sendWatchSyncPayload(String jsonPayload);
bool syncWatchTheme(WatchTransformTheme? theme);
/// Opens the system photo picker and returns a local PNG file path after
/// removing the image background on the host platform.
... ...