fix: auto repair bugs #53 #3

Merged
zyc merged 2 commits from fix/auto-20260228-145348 into main 2026-02-28 18:00:04 +08:00
11 changed files with 579 additions and 125 deletions
Showing only changes of commit b59a2da513 - Show all commits

View File

@ -2,6 +2,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/network/api_client.dart'; import '../../../../core/network/api_client.dart';
import '../../domain/entities/device.dart'; import '../../domain/entities/device.dart';
import '../../domain/entities/device_detail.dart'; import '../../domain/entities/device_detail.dart';
import '../../domain/entities/role_memory.dart';
part 'device_remote_data_source.g.dart'; part 'device_remote_data_source.g.dart';
@ -32,6 +33,9 @@ abstract class DeviceRemoteDataSource {
/// POST /devices/{id}/wifi/ /// POST /devices/{id}/wifi/
Future<void> configWifi(int userDeviceId, String ssid); Future<void> configWifi(int userDeviceId, String ssid);
/// GET /devices/role-memories/
Future<List<RoleMemory>> listRoleMemories({int? deviceTypeId});
} }
@riverpod @riverpod
@ -125,4 +129,18 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
data: {'ssid': ssid}, data: {'ssid': ssid},
); );
} }
@override
Future<List<RoleMemory>> listRoleMemories({int? deviceTypeId}) async {
final queryParams = <String, dynamic>{};
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<dynamic>;
return list
.map((e) => RoleMemory.fromJson(e as Map<String, dynamic>))
.toList();
}
} }

View File

@ -4,6 +4,7 @@ import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart'; import '../../../../core/errors/failures.dart';
import '../../domain/entities/device.dart'; import '../../domain/entities/device.dart';
import '../../domain/entities/device_detail.dart'; import '../../domain/entities/device_detail.dart';
import '../../domain/entities/role_memory.dart';
import '../../domain/repositories/device_repository.dart'; import '../../domain/repositories/device_repository.dart';
import '../datasources/device_remote_data_source.dart'; import '../datasources/device_remote_data_source.dart';
@ -144,4 +145,20 @@ class DeviceRepositoryImpl implements DeviceRepository {
return left(NetworkFailure(e.message)); return left(NetworkFailure(e.message));
} }
} }
@override
Future<Either<Failure, List<RoleMemory>>> 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));
}
}
} }

View File

@ -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<String, dynamic> json) =>
_$RoleMemoryFromJson(json);
}

View File

@ -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>(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<RoleMemory> get copyWith => _$RoleMemoryCopyWithImpl<RoleMemory>(this as RoleMemory, _$identity);
/// Serializes this RoleMemory to a JSON map.
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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

View File

@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'role_memory.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_RoleMemory _$RoleMemoryFromJson(Map<String, dynamic> 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<String, dynamic> _$RoleMemoryToJson(_RoleMemory instance) =>
<String, dynamic>{
'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,
};

View File

@ -2,6 +2,7 @@ import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/failures.dart'; import '../../../../core/errors/failures.dart';
import '../entities/device.dart'; import '../entities/device.dart';
import '../entities/device_detail.dart'; import '../entities/device_detail.dart';
import '../entities/role_memory.dart';
abstract class DeviceRepository { abstract class DeviceRepository {
Future<Either<Failure, Map<String, dynamic>>> queryByMac(String mac); Future<Either<Failure, Map<String, dynamic>>> queryByMac(String mac);
@ -13,4 +14,5 @@ abstract class DeviceRepository {
Future<Either<Failure, UserDevice>> updateSpirit(int userDeviceId, int spiritId); Future<Either<Failure, UserDevice>> updateSpirit(int userDeviceId, int spiritId);
Future<Either<Failure, void>> updateSettings(int userDeviceId, Map<String, dynamic> settings); Future<Either<Failure, void>> updateSettings(int userDeviceId, Map<String, dynamic> settings);
Future<Either<Failure, void>> configWifi(int userDeviceId, String ssid); Future<Either<Failure, void>> configWifi(int userDeviceId, String ssid);
Future<Either<Failure, List<RoleMemory>>> listRoleMemories({int? deviceTypeId});
} }

View File

@ -1,6 +1,7 @@
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/device.dart'; import '../../domain/entities/device.dart';
import '../../domain/entities/device_detail.dart'; import '../../domain/entities/device_detail.dart';
import '../../domain/entities/role_memory.dart';
import '../../data/repositories/device_repository_impl.dart'; import '../../data/repositories/device_repository_impl.dart';
part 'device_controller.g.dart'; part 'device_controller.g.dart';
@ -115,3 +116,22 @@ class DeviceDetailController extends _$DeviceDetailController {
ref.invalidateSelf(); ref.invalidateSelf();
} }
} }
///
@riverpod
class RoleMemoryController extends _$RoleMemoryController {
@override
FutureOr<List<RoleMemory>> build() async {
final repository = ref.read(deviceRepositoryProvider);
final result = await repository.listRoleMemories();
return result.fold(
(failure) => <RoleMemory>[],
(memories) => memories,
);
}
void refresh() {
if (!ref.mounted) return;
ref.invalidateSelf();
}
}

View File

@ -36,7 +36,7 @@ final class DeviceControllerProvider
DeviceController create() => DeviceController(); DeviceController create() => DeviceController();
} }
String _$deviceControllerHash() => r'3f73a13c7f93fecb9fe781efc4ee305b6186639e'; String _$deviceControllerHash() => r'94e697fab82bfeb03a25eb12fc548fed925ef5cc';
/// ///
@ -107,7 +107,7 @@ final class DeviceDetailControllerProvider
} }
String _$deviceDetailControllerHash() => String _$deviceDetailControllerHash() =>
r'1d9049597e39a0af3a70331378559aca0e1da54d'; r'd4e78c0f2298de55e7df31b4a34778b8169387a5';
/// ///
@ -161,3 +161,56 @@ abstract class _$DeviceDetailController extends $AsyncNotifier<DeviceDetail?> {
element.handleValue(ref, created); element.handleValue(ref, created);
} }
} }
///
@ProviderFor(RoleMemoryController)
const roleMemoryControllerProvider = RoleMemoryControllerProvider._();
///
final class RoleMemoryControllerProvider
extends $AsyncNotifierProvider<RoleMemoryController, List<RoleMemory>> {
///
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<List<RoleMemory>> {
FutureOr<List<RoleMemory>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<RoleMemory>>, List<RoleMemory>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<RoleMemory>>, List<RoleMemory>>,
AsyncValue<List<RoleMemory>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@ -174,12 +174,18 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
/// BLE API /// BLE API
Future<void> _startSearch() async { Future<void> _startSearch() async {
if (!_isBluetoothOn) { // Web : Web Bluetooth API
if (!kIsWeb && !_isBluetoothOn) {
_showBluetoothOffDialog(); _showBluetoothOffDialog();
return; return;
} }
// Web : startScan await
// Web Bluetooth API
// requestDevice
if (!kIsWeb) {
await _requestPermissions(); await _requestPermissions();
}
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {

View File

@ -3,9 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/theme/design_tokens.dart';
import 'package:airhub_app/widgets/animated_gradient_background.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart';
import 'package:airhub_app/widgets/glass_dialog.dart'; import 'package:airhub_app/widgets/glass_dialog.dart';
import 'package:airhub_app/widgets/ios_toast.dart'; import 'package:airhub_app/features/device/domain/entities/role_memory.dart';
import 'package:airhub_app/features/spirit/domain/entities/spirit.dart'; import 'package:airhub_app/features/device/presentation/controllers/device_controller.dart';
import 'package:airhub_app/features/spirit/presentation/controllers/spirit_controller.dart';
class AgentManagePage extends ConsumerStatefulWidget { class AgentManagePage extends ConsumerStatefulWidget {
const AgentManagePage({super.key}); const AgentManagePage({super.key});
@ -17,7 +16,7 @@ class AgentManagePage extends ConsumerStatefulWidget {
class _AgentManagePageState extends ConsumerState<AgentManagePage> { class _AgentManagePageState extends ConsumerState<AgentManagePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final spiritsAsync = ref.watch(spiritControllerProvider); final memoriesAsync = ref.watch(roleMemoryControllerProvider);
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
@ -28,7 +27,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
children: [ children: [
_buildHeader(context), _buildHeader(context),
Expanded( Expanded(
child: spiritsAsync.when( child: memoriesAsync.when(
loading: () => const Center( loading: () => const Center(
child: CircularProgressIndicator(color: Colors.white), child: CircularProgressIndicator(color: Colors.white),
), ),
@ -45,7 +44,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
GestureDetector( GestureDetector(
onTap: () => ref.read(spiritControllerProvider.notifier).refresh(), onTap: () => ref.read(roleMemoryControllerProvider.notifier).refresh(),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -61,8 +60,8 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
], ],
), ),
), ),
data: (spirits) { data: (memories) {
if (spirits.isEmpty) { if (memories.isEmpty) {
return Center( return Center(
child: Text( child: Text(
'暂无角色记忆', '暂无角色记忆',
@ -80,9 +79,9 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
right: 20, right: 20,
bottom: 40 + MediaQuery.of(context).padding.bottom, bottom: 40 + MediaQuery.of(context).padding.bottom,
), ),
itemCount: spirits.length, itemCount: memories.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _buildAgentCard(spirits[index]); return _buildMemoryCard(memories[index]);
}, },
); );
}, },
@ -159,16 +158,16 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
); );
} }
Widget _buildAgentCard(Spirit spirit) { Widget _buildMemoryCard(RoleMemory memory) {
final dateStr = spirit.createdAt != null final dateStr = memory.createdAt != null
? spirit.createdAt!.substring(0, 10).replaceAll('-', '/') ? memory.createdAt!.substring(0, 10).replaceAll('-', '/')
: ''; : '';
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFD4A373), // Fallback color: const Color(0xFFD4A373),
gradient: const LinearGradient(colors: AppColors.gradientCapybara), gradient: const LinearGradient(colors: AppColors.gradientCapybara),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
@ -181,7 +180,6 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
), ),
child: Stack( child: Stack(
children: [ children: [
// Top highlight layer
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -226,19 +224,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: spirit.avatar != null && spirit.avatar!.isNotEmpty child: const Text('🧠', style: TextStyle(fontSize: 24)),
? 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)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
@ -246,7 +232,9 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
spirit.name, memory.deviceTypeName.isNotEmpty
? memory.deviceTypeName
: '角色记忆 #${memory.id}',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -260,32 +248,36 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
], ],
), ),
), ),
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), 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), const SizedBox(height: 12),
Container(height: 1, color: Colors.white.withOpacity(0.2)), Container(height: 1, color: Colors.white.withOpacity(0.2)),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
_buildActionBtn( _buildStatusTag(memory.isBound),
'解绑',
isDanger: true,
onTap: () => _showUnbindDialog(spirit),
),
const SizedBox(width: 8),
_buildActionBtn(
'删除',
isDanger: true,
onTap: () => _showDeleteDialog(spirit),
),
], ],
), ),
], ],
@ -313,15 +305,8 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
); );
} }
Widget _buildActionBtn( Widget _buildStatusTag(bool isBound) {
String text, { return Container(
bool isDanger = false,
bool isInject = false,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
@ -331,66 +316,22 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (isDanger) ...[
Icon( Icon(
Icons.link_off, isBound ? Icons.link : Icons.link_off,
size: 14, size: 14,
color: AppColors.danger.withOpacity(0.9), color: Colors.white.withOpacity(0.9),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
] else if (isInject) ...[
Icon(Icons.download, size: 14, color: const Color(0xFFB07D5A)),
const SizedBox(width: 4),
],
Text( Text(
text, isBound ? '使用中' : '空闲',
style: TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isDanger color: Colors.white,
? AppColors.danger
: (isInject ? const Color(0xFFB07D5A) : 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}' : '删除失败');
}
},
); );
} }
} }

View File

@ -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/auth/presentation/controllers/auth_controller.dart';
import 'package:airhub_app/features/system/data/datasources/system_remote_data_source.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/device/presentation/controllers/device_controller.dart';
import 'package:airhub_app/features/user/presentation/controllers/user_controller.dart';
class SettingsPage extends ConsumerStatefulWidget { class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@ -26,6 +27,11 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// watch provider // watch provider
ref.watch(deviceControllerProvider); 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( return Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: Stack( body: Stack(
@ -48,8 +54,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
_buildItem( _buildItem(
'📱', '📱',
'绑定手机', '绑定手机',
value: '138****3069', value: maskedPhone.isNotEmpty ? maskedPhone : '未绑定',
onTap: () => _showMessage('绑定手机', '138****3069'), onTap: () => _showMessage('绑定手机', maskedPhone.isNotEmpty ? maskedPhone : '未绑定'),
), ),
_buildItem( _buildItem(
'🔐', '🔐',