From 3a89ad4b5573fb553230fc16f25a7980ef3fd005 Mon Sep 17 00:00:00 2001 From: repair-agent Date: Sat, 28 Feb 2026 14:57:20 +0800 Subject: [PATCH 1/2] fix: auto repair bugs #53 --- .../controllers/device_controller.dart | 7 +++++++ airhub_app/lib/pages/wifi_config_page.dart | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart index 4009689..f6a9a96 100644 --- a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart @@ -25,6 +25,7 @@ class DeviceController extends _$DeviceController { return result.fold( (failure) => false, (bindingId) { + if (!ref.mounted) return false; ref.invalidateSelf(); return true; }, @@ -38,6 +39,7 @@ class DeviceController extends _$DeviceController { return result.fold( (failure) => false, (_) { + if (!ref.mounted) return false; final current = state.value ?? []; state = AsyncData( current.where((d) => d.id != userDeviceId).toList(), @@ -54,6 +56,7 @@ class DeviceController extends _$DeviceController { return result.fold( (failure) => false, (updated) { + if (!ref.mounted) return false; ref.invalidateSelf(); return true; }, @@ -61,6 +64,7 @@ class DeviceController extends _$DeviceController { } void refresh() { + if (!ref.mounted) return; ref.invalidateSelf(); } } @@ -85,6 +89,7 @@ class DeviceDetailController extends _$DeviceDetailController { return result.fold( (failure) => false, (_) { + if (!ref.mounted) return false; ref.invalidateSelf(); return true; }, @@ -98,6 +103,7 @@ class DeviceDetailController extends _$DeviceDetailController { return result.fold( (failure) => false, (_) { + if (!ref.mounted) return false; ref.invalidateSelf(); return true; }, @@ -105,6 +111,7 @@ class DeviceDetailController extends _$DeviceDetailController { } void refresh() { + if (!ref.mounted) return; ref.invalidateSelf(); } } diff --git a/airhub_app/lib/pages/wifi_config_page.dart b/airhub_app/lib/pages/wifi_config_page.dart index c4f4cc7..faccfa5 100644 --- a/airhub_app/lib/pages/wifi_config_page.dart +++ b/airhub_app/lib/pages/wifi_config_page.dart @@ -29,6 +29,7 @@ class _WifiConfigPageState extends ConsumerState double _progress = 0.0; String _progressText = '正在连接WiFi...'; bool _connectFailed = false; + bool _isBinding = false; // Device Info Map _deviceInfo = {}; @@ -128,12 +129,20 @@ class _WifiConfigPageState extends ConsumerState if (_currentStep == 2 && _passwordController.text.isEmpty) return; if (_currentStep == 4) { + if (_isBinding) return; + setState(() => _isBinding = true); + final sn = _deviceInfo['sn'] as String? ?? ''; if (sn.isNotEmpty) { - debugPrint('[WiFi Config] Binding device sn=$sn'); - await ref.read(deviceControllerProvider.notifier).bindDevice(sn); + try { + debugPrint('[WiFi Config] Binding device sn=$sn'); + await ref.read(deviceControllerProvider.notifier).bindDevice(sn); + } catch (e) { + debugPrint('[WiFi Config] bindDevice 异常: $e'); + } } if (!mounted) return; + setState(() => _isBinding = false); context.go('/device-control'); return; } @@ -705,7 +714,7 @@ class _WifiConfigPageState extends ConsumerState } if (_currentStep == 4) { showNext = true; - nextText = '进入设备'; + nextText = _isBinding ? '绑定中...' : '进入设备'; } if (!showNext && _currentStep != 3) { @@ -764,7 +773,7 @@ class _WifiConfigPageState extends ConsumerState if (_currentStep < 4) const SizedBox(width: 16), GradientButton( text: nextText, - onPressed: _handleNext, + onPressed: _isBinding ? null : _handleNext, height: 56, width: _currentStep == 4 ? 200 : 160, ), -- 2.47.2 From b59a2da513582050ca7a3510708f043b17f557c1 Mon Sep 17 00:00:00 2001 From: repair-agent Date: Sat, 28 Feb 2026 15:35:12 +0800 Subject: [PATCH 2/2] fix: auto repair bugs #10 --- .../device_remote_data_source.dart | 18 + .../repositories/device_repository_impl.dart | 17 + .../device/domain/entities/role_memory.dart | 28 ++ .../domain/entities/role_memory.freezed.dart | 319 ++++++++++++++++++ .../device/domain/entities/role_memory.g.dart | 44 +++ .../repositories/device_repository.dart | 2 + .../controllers/device_controller.dart | 20 ++ .../controllers/device_controller.g.dart | 57 +++- airhub_app/lib/pages/bluetooth_page.dart | 10 +- .../lib/pages/profile/agent_manage_page.dart | 179 ++++------ .../lib/pages/profile/settings_page.dart | 10 +- 11 files changed, 579 insertions(+), 125 deletions(-) create mode 100644 airhub_app/lib/features/device/domain/entities/role_memory.dart create mode 100644 airhub_app/lib/features/device/domain/entities/role_memory.freezed.dart create mode 100644 airhub_app/lib/features/device/domain/entities/role_memory.g.dart diff --git a/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart index 3345d03..9f6b581 100644 --- a/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart +++ b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart @@ -2,6 +2,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../../core/network/api_client.dart'; import '../../domain/entities/device.dart'; import '../../domain/entities/device_detail.dart'; +import '../../domain/entities/role_memory.dart'; part 'device_remote_data_source.g.dart'; @@ -32,6 +33,9 @@ abstract class DeviceRemoteDataSource { /// POST /devices/{id}/wifi/ Future configWifi(int userDeviceId, String ssid); + + /// GET /devices/role-memories/ + Future> listRoleMemories({int? deviceTypeId}); } @riverpod @@ -125,4 +129,18 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource { data: {'ssid': ssid}, ); } + + @override + Future> listRoleMemories({int? deviceTypeId}) async { + final queryParams = {}; + if (deviceTypeId != null) queryParams['device_type_id'] = deviceTypeId; + final data = await _apiClient.get( + '/devices/role-memories/', + queryParameters: queryParams.isNotEmpty ? queryParams : null, + ); + final list = data as List; + return list + .map((e) => RoleMemory.fromJson(e as Map)) + .toList(); + } } diff --git a/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart b/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart index 8d646bd..72f9a7d 100644 --- a/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart +++ b/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart @@ -4,6 +4,7 @@ import '../../../../core/errors/exceptions.dart'; import '../../../../core/errors/failures.dart'; import '../../domain/entities/device.dart'; import '../../domain/entities/device_detail.dart'; +import '../../domain/entities/role_memory.dart'; import '../../domain/repositories/device_repository.dart'; import '../datasources/device_remote_data_source.dart'; @@ -144,4 +145,20 @@ class DeviceRepositoryImpl implements DeviceRepository { return left(NetworkFailure(e.message)); } } + + @override + Future>> listRoleMemories({ + int? deviceTypeId, + }) async { + try { + final result = await _remoteDataSource.listRoleMemories( + deviceTypeId: deviceTypeId, + ); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } } diff --git a/airhub_app/lib/features/device/domain/entities/role_memory.dart b/airhub_app/lib/features/device/domain/entities/role_memory.dart new file mode 100644 index 0000000..69d8ecf --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/role_memory.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'role_memory.freezed.dart'; +part 'role_memory.g.dart'; + +@freezed +abstract class RoleMemory with _$RoleMemory { + const factory RoleMemory({ + required int id, + required int deviceType, + @Default('') String deviceTypeName, + @Default(true) bool isBound, + @Default('') String nickname, + @Default('') String userName, + @Default(50) int volume, + @Default(50) int brightness, + @Default(true) bool allowInterrupt, + @Default(false) bool privacyMode, + @Default('') String prompt, + @Default('') String voiceId, + @Default('') String memorySummary, + String? createdAt, + String? updatedAt, + }) = _RoleMemory; + + factory RoleMemory.fromJson(Map json) => + _$RoleMemoryFromJson(json); +} diff --git a/airhub_app/lib/features/device/domain/entities/role_memory.freezed.dart b/airhub_app/lib/features/device/domain/entities/role_memory.freezed.dart new file mode 100644 index 0000000..2b82c80 --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/role_memory.freezed.dart @@ -0,0 +1,319 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'role_memory.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$RoleMemory { + + int get id; int get deviceType; String get deviceTypeName; bool get isBound; String get nickname; String get userName; int get volume; int get brightness; bool get allowInterrupt; bool get privacyMode; String get prompt; String get voiceId; String get memorySummary; String? get createdAt; String? get updatedAt; +/// Create a copy of RoleMemory +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RoleMemoryCopyWith get copyWith => _$RoleMemoryCopyWithImpl(this as RoleMemory, _$identity); + + /// Serializes this RoleMemory to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RoleMemory&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeName, deviceTypeName) || other.deviceTypeName == deviceTypeName)&&(identical(other.isBound, isBound) || other.isBound == isBound)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.volume, volume) || other.volume == volume)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.allowInterrupt, allowInterrupt) || other.allowInterrupt == allowInterrupt)&&(identical(other.privacyMode, privacyMode) || other.privacyMode == privacyMode)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.voiceId, voiceId) || other.voiceId == voiceId)&&(identical(other.memorySummary, memorySummary) || other.memorySummary == memorySummary)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,deviceType,deviceTypeName,isBound,nickname,userName,volume,brightness,allowInterrupt,privacyMode,prompt,voiceId,memorySummary,createdAt,updatedAt); + +@override +String toString() { + return 'RoleMemory(id: $id, deviceType: $deviceType, deviceTypeName: $deviceTypeName, isBound: $isBound, nickname: $nickname, userName: $userName, volume: $volume, brightness: $brightness, allowInterrupt: $allowInterrupt, privacyMode: $privacyMode, prompt: $prompt, voiceId: $voiceId, memorySummary: $memorySummary, createdAt: $createdAt, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $RoleMemoryCopyWith<$Res> { + factory $RoleMemoryCopyWith(RoleMemory value, $Res Function(RoleMemory) _then) = _$RoleMemoryCopyWithImpl; +@useResult +$Res call({ + int id, int deviceType, String deviceTypeName, bool isBound, String nickname, String userName, int volume, int brightness, bool allowInterrupt, bool privacyMode, String prompt, String voiceId, String memorySummary, String? createdAt, String? updatedAt +}); + + + + +} +/// @nodoc +class _$RoleMemoryCopyWithImpl<$Res> + implements $RoleMemoryCopyWith<$Res> { + _$RoleMemoryCopyWithImpl(this._self, this._then); + + final RoleMemory _self; + final $Res Function(RoleMemory) _then; + +/// Create a copy of RoleMemory +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceType = null,Object? deviceTypeName = null,Object? isBound = null,Object? nickname = null,Object? userName = null,Object? volume = null,Object? brightness = null,Object? allowInterrupt = null,Object? privacyMode = null,Object? prompt = null,Object? voiceId = null,Object? memorySummary = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,deviceType: null == deviceType ? _self.deviceType : deviceType // ignore: cast_nullable_to_non_nullable +as int,deviceTypeName: null == deviceTypeName ? _self.deviceTypeName : deviceTypeName // ignore: cast_nullable_to_non_nullable +as String,isBound: null == isBound ? _self.isBound : isBound // ignore: cast_nullable_to_non_nullable +as bool,nickname: null == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable +as String,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String,volume: null == volume ? _self.volume : volume // ignore: cast_nullable_to_non_nullable +as int,brightness: null == brightness ? _self.brightness : brightness // ignore: cast_nullable_to_non_nullable +as int,allowInterrupt: null == allowInterrupt ? _self.allowInterrupt : allowInterrupt // ignore: cast_nullable_to_non_nullable +as bool,privacyMode: null == privacyMode ? _self.privacyMode : privacyMode // ignore: cast_nullable_to_non_nullable +as bool,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,voiceId: null == voiceId ? _self.voiceId : voiceId // ignore: cast_nullable_to_non_nullable +as String,memorySummary: null == memorySummary ? _self.memorySummary : memorySummary // ignore: cast_nullable_to_non_nullable +as String,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [RoleMemory]. +extension RoleMemoryPatterns on RoleMemory { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _RoleMemory value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _RoleMemory() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _RoleMemory value) $default,){ +final _that = this; +switch (_that) { +case _RoleMemory(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _RoleMemory value)? $default,){ +final _that = this; +switch (_that) { +case _RoleMemory() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, int deviceType, String deviceTypeName, bool isBound, String nickname, String userName, int volume, int brightness, bool allowInterrupt, bool privacyMode, String prompt, String voiceId, String memorySummary, String? createdAt, String? updatedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _RoleMemory() when $default != null: +return $default(_that.id,_that.deviceType,_that.deviceTypeName,_that.isBound,_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode,_that.prompt,_that.voiceId,_that.memorySummary,_that.createdAt,_that.updatedAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, int deviceType, String deviceTypeName, bool isBound, String nickname, String userName, int volume, int brightness, bool allowInterrupt, bool privacyMode, String prompt, String voiceId, String memorySummary, String? createdAt, String? updatedAt) $default,) {final _that = this; +switch (_that) { +case _RoleMemory(): +return $default(_that.id,_that.deviceType,_that.deviceTypeName,_that.isBound,_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode,_that.prompt,_that.voiceId,_that.memorySummary,_that.createdAt,_that.updatedAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, int deviceType, String deviceTypeName, bool isBound, String nickname, String userName, int volume, int brightness, bool allowInterrupt, bool privacyMode, String prompt, String voiceId, String memorySummary, String? createdAt, String? updatedAt)? $default,) {final _that = this; +switch (_that) { +case _RoleMemory() when $default != null: +return $default(_that.id,_that.deviceType,_that.deviceTypeName,_that.isBound,_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode,_that.prompt,_that.voiceId,_that.memorySummary,_that.createdAt,_that.updatedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _RoleMemory implements RoleMemory { + const _RoleMemory({required this.id, required this.deviceType, this.deviceTypeName = '', this.isBound = true, this.nickname = '', this.userName = '', this.volume = 50, this.brightness = 50, this.allowInterrupt = true, this.privacyMode = false, this.prompt = '', this.voiceId = '', this.memorySummary = '', this.createdAt, this.updatedAt}); + factory _RoleMemory.fromJson(Map json) => _$RoleMemoryFromJson(json); + +@override final int id; +@override final int deviceType; +@override@JsonKey() final String deviceTypeName; +@override@JsonKey() final bool isBound; +@override@JsonKey() final String nickname; +@override@JsonKey() final String userName; +@override@JsonKey() final int volume; +@override@JsonKey() final int brightness; +@override@JsonKey() final bool allowInterrupt; +@override@JsonKey() final bool privacyMode; +@override@JsonKey() final String prompt; +@override@JsonKey() final String voiceId; +@override@JsonKey() final String memorySummary; +@override final String? createdAt; +@override final String? updatedAt; + +/// Create a copy of RoleMemory +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$RoleMemoryCopyWith<_RoleMemory> get copyWith => __$RoleMemoryCopyWithImpl<_RoleMemory>(this, _$identity); + +@override +Map toJson() { + return _$RoleMemoryToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RoleMemory&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeName, deviceTypeName) || other.deviceTypeName == deviceTypeName)&&(identical(other.isBound, isBound) || other.isBound == isBound)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.userName, userName) || other.userName == userName)&&(identical(other.volume, volume) || other.volume == volume)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.allowInterrupt, allowInterrupt) || other.allowInterrupt == allowInterrupt)&&(identical(other.privacyMode, privacyMode) || other.privacyMode == privacyMode)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.voiceId, voiceId) || other.voiceId == voiceId)&&(identical(other.memorySummary, memorySummary) || other.memorySummary == memorySummary)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,deviceType,deviceTypeName,isBound,nickname,userName,volume,brightness,allowInterrupt,privacyMode,prompt,voiceId,memorySummary,createdAt,updatedAt); + +@override +String toString() { + return 'RoleMemory(id: $id, deviceType: $deviceType, deviceTypeName: $deviceTypeName, isBound: $isBound, nickname: $nickname, userName: $userName, volume: $volume, brightness: $brightness, allowInterrupt: $allowInterrupt, privacyMode: $privacyMode, prompt: $prompt, voiceId: $voiceId, memorySummary: $memorySummary, createdAt: $createdAt, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$RoleMemoryCopyWith<$Res> implements $RoleMemoryCopyWith<$Res> { + factory _$RoleMemoryCopyWith(_RoleMemory value, $Res Function(_RoleMemory) _then) = __$RoleMemoryCopyWithImpl; +@override @useResult +$Res call({ + int id, int deviceType, String deviceTypeName, bool isBound, String nickname, String userName, int volume, int brightness, bool allowInterrupt, bool privacyMode, String prompt, String voiceId, String memorySummary, String? createdAt, String? updatedAt +}); + + + + +} +/// @nodoc +class __$RoleMemoryCopyWithImpl<$Res> + implements _$RoleMemoryCopyWith<$Res> { + __$RoleMemoryCopyWithImpl(this._self, this._then); + + final _RoleMemory _self; + final $Res Function(_RoleMemory) _then; + +/// Create a copy of RoleMemory +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceType = null,Object? deviceTypeName = null,Object? isBound = null,Object? nickname = null,Object? userName = null,Object? volume = null,Object? brightness = null,Object? allowInterrupt = null,Object? privacyMode = null,Object? prompt = null,Object? voiceId = null,Object? memorySummary = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { + return _then(_RoleMemory( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,deviceType: null == deviceType ? _self.deviceType : deviceType // ignore: cast_nullable_to_non_nullable +as int,deviceTypeName: null == deviceTypeName ? _self.deviceTypeName : deviceTypeName // ignore: cast_nullable_to_non_nullable +as String,isBound: null == isBound ? _self.isBound : isBound // ignore: cast_nullable_to_non_nullable +as bool,nickname: null == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable +as String,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String,volume: null == volume ? _self.volume : volume // ignore: cast_nullable_to_non_nullable +as int,brightness: null == brightness ? _self.brightness : brightness // ignore: cast_nullable_to_non_nullable +as int,allowInterrupt: null == allowInterrupt ? _self.allowInterrupt : allowInterrupt // ignore: cast_nullable_to_non_nullable +as bool,privacyMode: null == privacyMode ? _self.privacyMode : privacyMode // ignore: cast_nullable_to_non_nullable +as bool,prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,voiceId: null == voiceId ? _self.voiceId : voiceId // ignore: cast_nullable_to_non_nullable +as String,memorySummary: null == memorySummary ? _self.memorySummary : memorySummary // ignore: cast_nullable_to_non_nullable +as String,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/airhub_app/lib/features/device/domain/entities/role_memory.g.dart b/airhub_app/lib/features/device/domain/entities/role_memory.g.dart new file mode 100644 index 0000000..ac882e0 --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/role_memory.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'role_memory.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_RoleMemory _$RoleMemoryFromJson(Map json) => _RoleMemory( + id: (json['id'] as num).toInt(), + deviceType: (json['device_type'] as num).toInt(), + deviceTypeName: json['device_type_name'] as String? ?? '', + isBound: json['is_bound'] as bool? ?? true, + nickname: json['nickname'] as String? ?? '', + userName: json['user_name'] as String? ?? '', + volume: (json['volume'] as num?)?.toInt() ?? 50, + brightness: (json['brightness'] as num?)?.toInt() ?? 50, + allowInterrupt: json['allow_interrupt'] as bool? ?? true, + privacyMode: json['privacy_mode'] as bool? ?? false, + prompt: json['prompt'] as String? ?? '', + voiceId: json['voice_id'] as String? ?? '', + memorySummary: json['memory_summary'] as String? ?? '', + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, +); + +Map _$RoleMemoryToJson(_RoleMemory instance) => + { + 'id': instance.id, + 'device_type': instance.deviceType, + 'device_type_name': instance.deviceTypeName, + 'is_bound': instance.isBound, + 'nickname': instance.nickname, + 'user_name': instance.userName, + 'volume': instance.volume, + 'brightness': instance.brightness, + 'allow_interrupt': instance.allowInterrupt, + 'privacy_mode': instance.privacyMode, + 'prompt': instance.prompt, + 'voice_id': instance.voiceId, + 'memory_summary': instance.memorySummary, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, + }; diff --git a/airhub_app/lib/features/device/domain/repositories/device_repository.dart b/airhub_app/lib/features/device/domain/repositories/device_repository.dart index 9775b1c..9682d51 100644 --- a/airhub_app/lib/features/device/domain/repositories/device_repository.dart +++ b/airhub_app/lib/features/device/domain/repositories/device_repository.dart @@ -2,6 +2,7 @@ import 'package:fpdart/fpdart.dart'; import '../../../../core/errors/failures.dart'; import '../entities/device.dart'; import '../entities/device_detail.dart'; +import '../entities/role_memory.dart'; abstract class DeviceRepository { Future>> queryByMac(String mac); @@ -13,4 +14,5 @@ abstract class DeviceRepository { Future> updateSpirit(int userDeviceId, int spiritId); Future> updateSettings(int userDeviceId, Map settings); Future> configWifi(int userDeviceId, String ssid); + Future>> listRoleMemories({int? deviceTypeId}); } diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart index f6a9a96..aca2810 100644 --- a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart @@ -1,6 +1,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../domain/entities/device.dart'; import '../../domain/entities/device_detail.dart'; +import '../../domain/entities/role_memory.dart'; import '../../data/repositories/device_repository_impl.dart'; part 'device_controller.g.dart'; @@ -115,3 +116,22 @@ class DeviceDetailController extends _$DeviceDetailController { ref.invalidateSelf(); } } + +/// 管理角色记忆列表 +@riverpod +class RoleMemoryController extends _$RoleMemoryController { + @override + FutureOr> build() async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.listRoleMemories(); + return result.fold( + (failure) => [], + (memories) => memories, + ); + } + + void refresh() { + if (!ref.mounted) return; + ref.invalidateSelf(); + } +} diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart index 84e11e3..46f4b19 100644 --- a/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart @@ -36,7 +36,7 @@ final class DeviceControllerProvider DeviceController create() => DeviceController(); } -String _$deviceControllerHash() => r'3f73a13c7f93fecb9fe781efc4ee305b6186639e'; +String _$deviceControllerHash() => r'94e697fab82bfeb03a25eb12fc548fed925ef5cc'; /// 管理用户设备列表 @@ -107,7 +107,7 @@ final class DeviceDetailControllerProvider } String _$deviceDetailControllerHash() => - r'1d9049597e39a0af3a70331378559aca0e1da54d'; + r'd4e78c0f2298de55e7df31b4a34778b8169387a5'; /// 管理单个设备详情 @@ -161,3 +161,56 @@ abstract class _$DeviceDetailController extends $AsyncNotifier { element.handleValue(ref, created); } } + +/// 管理角色记忆列表 + +@ProviderFor(RoleMemoryController) +const roleMemoryControllerProvider = RoleMemoryControllerProvider._(); + +/// 管理角色记忆列表 +final class RoleMemoryControllerProvider + extends $AsyncNotifierProvider> { + /// 管理角色记忆列表 + const RoleMemoryControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'roleMemoryControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$roleMemoryControllerHash(); + + @$internal + @override + RoleMemoryController create() => RoleMemoryController(); +} + +String _$roleMemoryControllerHash() => + r'e02cd6952277bf766c0b657979b28f4bf8e98c1b'; + +/// 管理角色记忆列表 + +abstract class _$RoleMemoryController extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/airhub_app/lib/pages/bluetooth_page.dart b/airhub_app/lib/pages/bluetooth_page.dart index a1a989f..cd94aae 100644 --- a/airhub_app/lib/pages/bluetooth_page.dart +++ b/airhub_app/lib/pages/bluetooth_page.dart @@ -174,12 +174,18 @@ class _BluetoothPageState extends ConsumerState /// 开始 BLE 扫描(持续扫描,直到找到设备并完成 API 查询) Future _startSearch() async { - if (!_isBluetoothOn) { + // Web 平台: 跳过蓝牙状态检查(Web Bluetooth API 会自行处理可用性) + if (!kIsWeb && !_isBluetoothOn) { _showBluetoothOffDialog(); return; } - await _requestPermissions(); + // Web 平台: 不能在 startScan 前 await 任何异步操作, + // 否则会丢失用户手势上下文(Web Bluetooth API 要求 + // requestDevice 必须在用户手势的同步调用链中触发) + if (!kIsWeb) { + await _requestPermissions(); + } if (!mounted) return; setState(() { diff --git a/airhub_app/lib/pages/profile/agent_manage_page.dart b/airhub_app/lib/pages/profile/agent_manage_page.dart index 7ffef35..68b4353 100644 --- a/airhub_app/lib/pages/profile/agent_manage_page.dart +++ b/airhub_app/lib/pages/profile/agent_manage_page.dart @@ -3,9 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart'; import 'package:airhub_app/widgets/glass_dialog.dart'; -import 'package:airhub_app/widgets/ios_toast.dart'; -import 'package:airhub_app/features/spirit/domain/entities/spirit.dart'; -import 'package:airhub_app/features/spirit/presentation/controllers/spirit_controller.dart'; +import 'package:airhub_app/features/device/domain/entities/role_memory.dart'; +import 'package:airhub_app/features/device/presentation/controllers/device_controller.dart'; class AgentManagePage extends ConsumerStatefulWidget { const AgentManagePage({super.key}); @@ -17,7 +16,7 @@ class AgentManagePage extends ConsumerStatefulWidget { class _AgentManagePageState extends ConsumerState { @override Widget build(BuildContext context) { - final spiritsAsync = ref.watch(spiritControllerProvider); + final memoriesAsync = ref.watch(roleMemoryControllerProvider); return Scaffold( backgroundColor: Colors.transparent, @@ -28,7 +27,7 @@ class _AgentManagePageState extends ConsumerState { children: [ _buildHeader(context), Expanded( - child: spiritsAsync.when( + child: memoriesAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: Colors.white), ), @@ -45,7 +44,7 @@ class _AgentManagePageState extends ConsumerState { ), const SizedBox(height: 12), GestureDetector( - onTap: () => ref.read(spiritControllerProvider.notifier).refresh(), + onTap: () => ref.read(roleMemoryControllerProvider.notifier).refresh(), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), decoration: BoxDecoration( @@ -61,8 +60,8 @@ class _AgentManagePageState extends ConsumerState { ], ), ), - data: (spirits) { - if (spirits.isEmpty) { + data: (memories) { + if (memories.isEmpty) { return Center( child: Text( '暂无角色记忆', @@ -80,9 +79,9 @@ class _AgentManagePageState extends ConsumerState { right: 20, bottom: 40 + MediaQuery.of(context).padding.bottom, ), - itemCount: spirits.length, + itemCount: memories.length, itemBuilder: (context, index) { - return _buildAgentCard(spirits[index]); + return _buildMemoryCard(memories[index]); }, ); }, @@ -159,16 +158,16 @@ class _AgentManagePageState extends ConsumerState { ); } - Widget _buildAgentCard(Spirit spirit) { - final dateStr = spirit.createdAt != null - ? spirit.createdAt!.substring(0, 10).replaceAll('-', '/') + Widget _buildMemoryCard(RoleMemory memory) { + final dateStr = memory.createdAt != null + ? memory.createdAt!.substring(0, 10).replaceAll('-', '/') : ''; return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFFD4A373), // Fallback + color: const Color(0xFFD4A373), gradient: const LinearGradient(colors: AppColors.gradientCapybara), borderRadius: BorderRadius.circular(20), boxShadow: [ @@ -181,7 +180,6 @@ class _AgentManagePageState extends ConsumerState { ), child: Stack( children: [ - // Top highlight layer Positioned( left: 0, right: 0, @@ -226,19 +224,7 @@ class _AgentManagePageState extends ConsumerState { borderRadius: BorderRadius.circular(12), ), alignment: Alignment.center, - child: spirit.avatar != null && spirit.avatar!.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - spirit.avatar!, - width: 48, - height: 48, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - const Text('🧠', style: TextStyle(fontSize: 24)), - ), - ) - : const Text('🧠', style: TextStyle(fontSize: 24)), + child: const Text('🧠', style: TextStyle(fontSize: 24)), ), const SizedBox(width: 12), Expanded( @@ -246,7 +232,9 @@ class _AgentManagePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - spirit.name, + memory.deviceTypeName.isNotEmpty + ? memory.deviceTypeName + : '角色记忆 #${memory.id}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -260,32 +248,36 @@ class _AgentManagePageState extends ConsumerState { ], ), ), + if (memory.nickname.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + memory.nickname, + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.8), + ), + ), + ], ], ), ), ], ), const SizedBox(height: 12), - _buildDetailRow('状态:', spirit.isActive ? '活跃' : '未激活'), - + _buildDetailRow('状态:', memory.isBound ? '已绑定' : '空闲'), + if (memory.memorySummary.isNotEmpty) ...[ + const SizedBox(height: 6), + _buildDetailRow('记忆:', memory.memorySummary.length > 30 + ? '${memory.memorySummary.substring(0, 30)}...' + : memory.memorySummary), + ], const SizedBox(height: 12), Container(height: 1, color: Colors.white.withOpacity(0.2)), const SizedBox(height: 12), - Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - _buildActionBtn( - '解绑', - isDanger: true, - onTap: () => _showUnbindDialog(spirit), - ), - const SizedBox(width: 8), - _buildActionBtn( - '删除', - isDanger: true, - onTap: () => _showDeleteDialog(spirit), - ), + _buildStatusTag(memory.isBound), ], ), ], @@ -313,84 +305,33 @@ class _AgentManagePageState extends ConsumerState { ); } - Widget _buildActionBtn( - String text, { - bool isDanger = false, - bool isInject = false, - VoidCallback? onTap, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withOpacity(0.3)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isDanger) ...[ - Icon( - Icons.link_off, - size: 14, - color: AppColors.danger.withOpacity(0.9), - ), - const SizedBox(width: 4), - ] else if (isInject) ...[ - Icon(Icons.download, size: 14, color: const Color(0xFFB07D5A)), - const SizedBox(width: 4), - ], - Text( - text, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: isDanger - ? AppColors.danger - : (isInject ? const Color(0xFFB07D5A) : Colors.white), - ), + Widget _buildStatusTag(bool isBound) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isBound ? Icons.link : Icons.link_off, + size: 14, + color: Colors.white.withOpacity(0.9), + ), + const SizedBox(width: 4), + Text( + isBound ? '使用中' : '空闲', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.white, ), - ], - ), + ), + ], ), ); } - - void _showUnbindDialog(Spirit spirit) { - showGlassDialog( - context: context, - title: '确认解绑角色记忆?', - description: '解绑后,该角色记忆将与当前设备断开连接,但数据会保留在云端。', - cancelText: '取消', - confirmText: '确认解绑', - isDanger: true, - onConfirm: () async { - Navigator.pop(context); // Close dialog - final success = await ref.read(spiritControllerProvider.notifier).unbind(spirit.id); - if (mounted) { - AppToast.show(context, success ? '已解绑: ${spirit.name}' : '解绑失败'); - } - }, - ); - } - - void _showDeleteDialog(Spirit spirit) { - showGlassDialog( - context: context, - title: '确认删除角色记忆?', - description: '删除后,该角色记忆数据将无法恢复。', - cancelText: '取消', - confirmText: '确认删除', - isDanger: true, - onConfirm: () async { - Navigator.pop(context); - final success = await ref.read(spiritControllerProvider.notifier).delete(spirit.id); - if (mounted) { - AppToast.show(context, success ? '已删除: ${spirit.name}' : '删除失败'); - } - }, - ); - } } diff --git a/airhub_app/lib/pages/profile/settings_page.dart b/airhub_app/lib/pages/profile/settings_page.dart index 89aaf8d..d69bdcc 100644 --- a/airhub_app/lib/pages/profile/settings_page.dart +++ b/airhub_app/lib/pages/profile/settings_page.dart @@ -11,6 +11,7 @@ import 'package:airhub_app/widgets/glass_dialog.dart'; import 'package:airhub_app/features/auth/presentation/controllers/auth_controller.dart'; import 'package:airhub_app/features/system/data/datasources/system_remote_data_source.dart'; import 'package:airhub_app/features/device/presentation/controllers/device_controller.dart'; +import 'package:airhub_app/features/user/presentation/controllers/user_controller.dart'; class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @@ -26,6 +27,11 @@ class _SettingsPageState extends ConsumerState { Widget build(BuildContext context) { // watch 保持 provider 存活,确保硬件信息可用 ref.watch(deviceControllerProvider); + final user = ref.watch(userControllerProvider).value; + final phone = user?.phone ?? ''; + final maskedPhone = phone.length >= 7 + ? '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}' + : phone; return Scaffold( backgroundColor: Colors.transparent, body: Stack( @@ -48,8 +54,8 @@ class _SettingsPageState extends ConsumerState { _buildItem( '📱', '绑定手机', - value: '138****3069', - onTap: () => _showMessage('绑定手机', '138****3069'), + value: maskedPhone.isNotEmpty ? maskedPhone : '未绑定', + onTap: () => _showMessage('绑定手机', maskedPhone.isNotEmpty ? maskedPhone : '未绑定'), ), _buildItem( '🔐', -- 2.47.2