Compare commits
No commits in common. "main" and "feat/android-oneclick-login" have entirely different histories.
main
...
feat/andro
@ -20,5 +20,7 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -2,15 +2,12 @@ import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@ -26,42 +24,6 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要位置权限以扫描附近的蓝牙设备</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
<string>LaunchBackgroundColor</string>
|
||||
<key>UIImageName</key>
|
||||
<string>LaunchImage</string>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@ -79,5 +41,22 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要位置权限以扫描附近的蓝牙设备</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
<string>LaunchBackgroundColor</string>
|
||||
<key>UIImageName</key>
|
||||
<string>LaunchImage</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -5,7 +5,6 @@ import '../../features/auth/presentation/pages/login_page.dart';
|
||||
import '../../pages/bluetooth_page.dart';
|
||||
import '../../pages/device_control_page.dart';
|
||||
import '../../pages/home_page.dart';
|
||||
import '../../pages/product_selection_page.dart';
|
||||
import '../../pages/profile/profile_page.dart';
|
||||
import '../../pages/webview_page.dart';
|
||||
import '../../pages/wifi_config_page.dart';
|
||||
@ -49,10 +48,6 @@ GoRouter goRouter(Ref ref) {
|
||||
extra: state.extra as Map<String, dynamic>?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/product-selection',
|
||||
builder: (context, state) => const ProductSelectionPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/device-control',
|
||||
builder: (context, state) => const DeviceControlPage(),
|
||||
|
||||
@ -48,4 +48,4 @@ final class GoRouterProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$goRouterHash() => r'9f77a00bcbc90890c4b6594a9709288e5206c7d8';
|
||||
String _$goRouterHash() => r'8e620e452bb81f2c6ed87b136283a9e508dca2e9';
|
||||
|
||||
@ -16,6 +16,7 @@ class LogCenterService {
|
||||
defaultValue: 'https://qiyuan-log-center-api.airlabs.art',
|
||||
);
|
||||
static const String _url = '$_baseUrl/api/v1/logs/report';
|
||||
static const String _registerUrl = '$_baseUrl/api/v1/projects/register';
|
||||
static const String _projectId = 'airhub_app';
|
||||
static const String _repoUrl = 'https://gitea.airlabs.art/zyc/rtc_prd.git';
|
||||
|
||||
@ -29,6 +30,24 @@ class LogCenterService {
|
||||
));
|
||||
}
|
||||
|
||||
/// 初始化:向 Log Center 注册项目
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
await _dio.post(_registerUrl, data: {
|
||||
'project_id': _projectId,
|
||||
'repo_url': _repoUrl,
|
||||
'description': 'Flutter mobile app (subdirectory of rtc_prd repo)',
|
||||
'platform': defaultTargetPlatform.name,
|
||||
'environment': const String.fromEnvironment(
|
||||
'ENVIRONMENT',
|
||||
defaultValue: kDebugMode ? 'development' : 'production',
|
||||
),
|
||||
});
|
||||
} catch (_) {
|
||||
// 静默失败,不影响 App 启动
|
||||
}
|
||||
}
|
||||
|
||||
/// 上报 Flutter 框架错误(FlutterError)
|
||||
void reportFlutterError(FlutterErrorDetails details) {
|
||||
_report(
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'phone_auth_service_stub.dart'
|
||||
if (dart.library.io) 'package:ali_auth/ali_auth.dart';
|
||||
import 'package:ali_auth/ali_auth.dart';
|
||||
|
||||
part 'phone_auth_service.g.dart';
|
||||
|
||||
|
||||
@ -1,76 +1,15 @@
|
||||
/// Web 平台 stub — ali_auth 依赖运营商 SIM 卡,Web 上不可用。
|
||||
/// 仅提供编译所需的空壳,运行时由 kIsWeb 守卫跳过。
|
||||
|
||||
/// Web 平台 stub — ali_auth 不支持 Web,提供空实现
|
||||
class AliAuth {
|
||||
static Future<dynamic> initSdk(dynamic model) async {}
|
||||
static loginListen({
|
||||
bool type = true,
|
||||
required Function onEvent,
|
||||
Function? onError,
|
||||
bool isOnlyOne = true,
|
||||
}) {}
|
||||
static Future<void> initSdk(dynamic model) async {}
|
||||
static void loginListen({required Function(Map<String, dynamic>) onEvent}) {}
|
||||
static Future<dynamic> login({int timeout = 5000}) async {}
|
||||
static Future<void> quitPage() async {}
|
||||
static Future<void> hideLoading() async {}
|
||||
}
|
||||
|
||||
enum PageType {
|
||||
fullPort,
|
||||
fullLand,
|
||||
dialogPort,
|
||||
dialogLand,
|
||||
dialogBottom,
|
||||
customView,
|
||||
customXml,
|
||||
customGif,
|
||||
customMOV,
|
||||
customPIC,
|
||||
}
|
||||
|
||||
class AliAuthModel {
|
||||
AliAuthModel(
|
||||
String? androidSk,
|
||||
String? iosSk, {
|
||||
dynamic isDebug,
|
||||
dynamic isDelay,
|
||||
dynamic autoQuitPage,
|
||||
dynamic pageType,
|
||||
dynamic dialogWidth,
|
||||
dynamic dialogHeight,
|
||||
dynamic dialogCornerRadiusArray,
|
||||
dynamic dialogAlpha,
|
||||
dynamic tapAuthPageMaskClosePage,
|
||||
dynamic navHidden,
|
||||
dynamic navReturnHidden,
|
||||
dynamic logoHidden,
|
||||
dynamic logoImgPath,
|
||||
dynamic logoWidth,
|
||||
dynamic logoHeight,
|
||||
dynamic logoOffsetY,
|
||||
dynamic numberColor,
|
||||
dynamic numFieldOffsetY,
|
||||
dynamic sloganHidden,
|
||||
dynamic logBtnText,
|
||||
dynamic logBtnTextColor,
|
||||
dynamic logBtnWidth,
|
||||
dynamic logBtnHeight,
|
||||
dynamic logBtnOffsetY,
|
||||
dynamic switchAccText,
|
||||
dynamic switchAccTextColor,
|
||||
dynamic switchOffsetY,
|
||||
dynamic privacyState,
|
||||
dynamic privacyBefore,
|
||||
dynamic privacyEnd,
|
||||
dynamic vendorPrivacyPrefix,
|
||||
dynamic vendorPrivacySuffix,
|
||||
dynamic protocolOneName,
|
||||
dynamic protocolOneURL,
|
||||
dynamic protocolTwoName,
|
||||
dynamic protocolTwoURL,
|
||||
dynamic privacyOffsetY_B,
|
||||
dynamic pageBackgroundPath,
|
||||
dynamic backgroundColor,
|
||||
dynamic pageBackgroundRadius,
|
||||
dynamic isHiddenCustom,
|
||||
});
|
||||
AliAuthModel(String androidSk, String iosSk,
|
||||
{bool isDebug = false, bool autoQuitPage = false, dynamic pageType});
|
||||
}
|
||||
|
||||
class PageType {
|
||||
static const fullPort = 0;
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ 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';
|
||||
|
||||
@ -33,9 +32,6 @@ abstract class DeviceRemoteDataSource {
|
||||
|
||||
/// POST /devices/{id}/wifi/
|
||||
Future<void> configWifi(int userDeviceId, String ssid);
|
||||
|
||||
/// GET /devices/role-memories/
|
||||
Future<List<RoleMemory>> listRoleMemories({int? deviceTypeId});
|
||||
}
|
||||
|
||||
@riverpod
|
||||
@ -49,15 +45,6 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
|
||||
DeviceRemoteDataSourceImpl(this._apiClient);
|
||||
|
||||
/// Normalize spirit field: if API returns a Map (nested object), extract its 'id'.
|
||||
Map<String, dynamic> _normalizeSpiritField(Map<String, dynamic> json) {
|
||||
final spirit = json['spirit'];
|
||||
if (spirit is Map<String, dynamic>) {
|
||||
json['spirit'] = spirit['id'];
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> queryByMac(String mac) async {
|
||||
final data = await _apiClient.get(
|
||||
@ -78,10 +65,6 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
final body = <String, dynamic>{'sn': sn};
|
||||
if (spiritId != null) body['spirit_id'] = spiritId;
|
||||
final data = await _apiClient.post('/devices/bind/', data: body);
|
||||
if (data is Map<String, dynamic>) {
|
||||
_normalizeSpiritField(data);
|
||||
return data['id'] as int;
|
||||
}
|
||||
return data as int;
|
||||
}
|
||||
|
||||
@ -129,18 +112,4 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ 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';
|
||||
|
||||
@ -145,20 +144,4 @@ class DeviceRepositoryImpl implements DeviceRepository {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,6 @@ abstract class DeviceInfo with _$DeviceInfo {
|
||||
String? macAddress,
|
||||
@Default('') String name,
|
||||
@Default('in_stock') String status,
|
||||
@Default(false) bool isOnline,
|
||||
@Default('') String firmwareVersion,
|
||||
String? lastOnlineAt,
|
||||
String? createdAt,
|
||||
|
||||
@ -296,7 +296,7 @@ as String?,
|
||||
/// @nodoc
|
||||
mixin _$DeviceInfo {
|
||||
|
||||
int get id; String get sn; DeviceType? get deviceType; DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; bool get isOnline; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt;
|
||||
int get id; String get sn; DeviceType? get deviceType; DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt;
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -309,16 +309,16 @@ $DeviceInfoCopyWith<DeviceInfo> get copyWith => _$DeviceInfoCopyWithImpl<DeviceI
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
|
||||
@ -329,7 +329,7 @@ abstract mixin class $DeviceInfoCopyWith<$Res> {
|
||||
factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
});
|
||||
|
||||
|
||||
@ -346,7 +346,7 @@ class _$DeviceInfoCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? isOnline = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
|
||||
@ -355,8 +355,7 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
|
||||
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
|
||||
as bool,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,lastOnlineAt: freezed == lastOnlineAt ? _self.lastOnlineAt : lastOnlineAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
@ -468,10 +467,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@ -489,10 +488,10 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo():
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@ -509,10 +508,10 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@ -524,7 +523,7 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
|
||||
@JsonSerializable()
|
||||
|
||||
class _DeviceInfo implements DeviceInfo {
|
||||
const _DeviceInfo({required this.id, required this.sn, this.deviceType, this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.isOnline = false, this.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
|
||||
const _DeviceInfo({required this.id, required this.sn, this.deviceType, this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
|
||||
factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json);
|
||||
|
||||
@override final int id;
|
||||
@ -534,7 +533,6 @@ class _DeviceInfo implements DeviceInfo {
|
||||
@override final String? macAddress;
|
||||
@override@JsonKey() final String name;
|
||||
@override@JsonKey() final String status;
|
||||
@override@JsonKey() final bool isOnline;
|
||||
@override@JsonKey() final String firmwareVersion;
|
||||
@override final String? lastOnlineAt;
|
||||
@override final String? createdAt;
|
||||
@ -552,16 +550,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.lastOnlineAt, lastOnlineAt) || other.lastOnlineAt == lastOnlineAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,isOnline,firmwareVersion,lastOnlineAt,createdAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,sn,deviceType,deviceTypeInfo,macAddress,name,status,firmwareVersion,lastOnlineAt,createdAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, isOnline: $isOnline, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)';
|
||||
}
|
||||
|
||||
|
||||
@ -572,7 +570,7 @@ abstract mixin class _$DeviceInfoCopyWith<$Res> implements $DeviceInfoCopyWith<$
|
||||
factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt
|
||||
});
|
||||
|
||||
|
||||
@ -589,7 +587,7 @@ class __$DeviceInfoCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of DeviceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? isOnline = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? sn = null,Object? deviceType = freezed,Object? deviceTypeInfo = freezed,Object? macAddress = freezed,Object? name = null,Object? status = null,Object? firmwareVersion = null,Object? lastOnlineAt = freezed,Object? createdAt = freezed,}) {
|
||||
return _then(_DeviceInfo(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as int,sn: null == sn ? _self.sn : sn // ignore: cast_nullable_to_non_nullable
|
||||
@ -598,8 +596,7 @@ as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo
|
||||
as DeviceType?,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable
|
||||
as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
|
||||
as bool,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable
|
||||
as String,lastOnlineAt: freezed == lastOnlineAt ? _self.lastOnlineAt : lastOnlineAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
|
||||
@ -30,16 +30,15 @@ Map<String, dynamic> _$DeviceTypeToJson(_DeviceType instance) =>
|
||||
_DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
|
||||
id: (json['id'] as num).toInt(),
|
||||
sn: json['sn'] as String,
|
||||
deviceType: json['device_type'] == null
|
||||
? null
|
||||
: DeviceType.fromJson(json['device_type'] as Map<String, dynamic>),
|
||||
deviceTypeInfo: json['device_type_info'] == null
|
||||
? null
|
||||
: DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>),
|
||||
deviceType: (json['device_type'] is Map<String, dynamic>)
|
||||
? DeviceType.fromJson(json['device_type'] as Map<String, dynamic>)
|
||||
: null,
|
||||
deviceTypeInfo: (json['device_type_info'] is Map<String, dynamic>)
|
||||
? DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>)
|
||||
: null,
|
||||
macAddress: json['mac_address'] as String?,
|
||||
name: json['name'] as String? ?? '',
|
||||
status: json['status'] as String? ?? 'in_stock',
|
||||
isOnline: json['is_online'] as bool? ?? false,
|
||||
firmwareVersion: json['firmware_version'] as String? ?? '',
|
||||
lastOnlineAt: json['last_online_at'] as String?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
@ -54,7 +53,6 @@ Map<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
|
||||
'mac_address': instance.macAddress,
|
||||
'name': instance.name,
|
||||
'status': instance.status,
|
||||
'is_online': instance.isOnline,
|
||||
'firmware_version': instance.firmwareVersion,
|
||||
'last_online_at': instance.lastOnlineAt,
|
||||
'created_at': instance.createdAt,
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,319 +0,0 @@
|
||||
// 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
|
||||
@ -1,44 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
@ -2,7 +2,6 @@ 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<Either<Failure, Map<String, dynamic>>> queryByMac(String mac);
|
||||
@ -14,5 +13,4 @@ abstract class DeviceRepository {
|
||||
Future<Either<Failure, UserDevice>> updateSpirit(int userDeviceId, int spiritId);
|
||||
Future<Either<Failure, void>> updateSettings(int userDeviceId, Map<String, dynamic> settings);
|
||||
Future<Either<Failure, void>> configWifi(int userDeviceId, String ssid);
|
||||
Future<Either<Failure, List<RoleMemory>>> listRoleMemories({int? deviceTypeId});
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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';
|
||||
@ -22,11 +21,9 @@ class DeviceController extends _$DeviceController {
|
||||
Future<bool> bindDevice(String sn, {int? spiritId}) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.bindDevice(sn, spiritId: spiritId);
|
||||
if (!ref.mounted) return false;
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(bindingId) {
|
||||
if (!ref.mounted) return false;
|
||||
ref.invalidateSelf();
|
||||
return true;
|
||||
},
|
||||
@ -36,11 +33,9 @@ class DeviceController extends _$DeviceController {
|
||||
Future<bool> unbindDevice(int userDeviceId) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.unbindDevice(userDeviceId);
|
||||
if (!ref.mounted) return false;
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(_) {
|
||||
if (!ref.mounted) return false;
|
||||
final current = state.value ?? [];
|
||||
state = AsyncData(
|
||||
current.where((d) => d.id != userDeviceId).toList(),
|
||||
@ -53,11 +48,9 @@ class DeviceController extends _$DeviceController {
|
||||
Future<bool> updateSpirit(int userDeviceId, int spiritId) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.updateSpirit(userDeviceId, spiritId);
|
||||
if (!ref.mounted) return false;
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(updated) {
|
||||
if (!ref.mounted) return false;
|
||||
ref.invalidateSelf();
|
||||
return true;
|
||||
},
|
||||
@ -65,7 +58,6 @@ class DeviceController extends _$DeviceController {
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (!ref.mounted) return;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
@ -86,11 +78,9 @@ class DeviceDetailController extends _$DeviceDetailController {
|
||||
Future<bool> updateSettings(Map<String, dynamic> settings) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.updateSettings(userDeviceId, settings);
|
||||
if (!ref.mounted) return false;
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(_) {
|
||||
if (!ref.mounted) return false;
|
||||
ref.invalidateSelf();
|
||||
return true;
|
||||
},
|
||||
@ -100,11 +90,9 @@ class DeviceDetailController extends _$DeviceDetailController {
|
||||
Future<bool> configWifi(String ssid) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.configWifi(userDeviceId, ssid);
|
||||
if (!ref.mounted) return false;
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(_) {
|
||||
if (!ref.mounted) return false;
|
||||
ref.invalidateSelf();
|
||||
return true;
|
||||
},
|
||||
@ -112,26 +100,6 @@ class DeviceDetailController extends _$DeviceDetailController {
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (!ref.mounted) return;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ final class DeviceControllerProvider
|
||||
DeviceController create() => DeviceController();
|
||||
}
|
||||
|
||||
String _$deviceControllerHash() => r'94e697fab82bfeb03a25eb12fc548fed925ef5cc';
|
||||
String _$deviceControllerHash() => r'9b39117bd54964ba0035aad0eca10250454efaa7';
|
||||
|
||||
/// 管理用户设备列表
|
||||
|
||||
@ -107,7 +107,7 @@ final class DeviceDetailControllerProvider
|
||||
}
|
||||
|
||||
String _$deviceDetailControllerHash() =>
|
||||
r'd4e78c0f2298de55e7df31b4a34778b8169387a5';
|
||||
r'1d9049597e39a0af3a70331378559aca0e1da54d';
|
||||
|
||||
/// 管理单个设备详情
|
||||
|
||||
@ -161,56 +161,3 @@ abstract class _$DeviceDetailController extends $AsyncNotifier<DeviceDetail?> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ void main() {
|
||||
|
||||
final container = ProviderContainer();
|
||||
final logCenter = container.read(logCenterServiceProvider);
|
||||
logCenter.initialize();
|
||||
|
||||
// 捕获 Flutter 框架错误(Widget build 异常等)
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details); // 保留控制台输出
|
||||
|
||||
@ -174,18 +174,12 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
|
||||
|
||||
/// 开始 BLE 扫描(持续扫描,直到找到设备并完成 API 查询)
|
||||
Future<void> _startSearch() async {
|
||||
// Web 平台: 跳过蓝牙状态检查(Web Bluetooth API 会自行处理可用性)
|
||||
if (!kIsWeb && !_isBluetoothOn) {
|
||||
if (!_isBluetoothOn) {
|
||||
_showBluetoothOffDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// Web 平台: 不能在 startScan 前 await 任何异步操作,
|
||||
// 否则会丢失用户手势上下文(Web Bluetooth API 要求
|
||||
// requestDevice 必须在用户手势的同步调用链中触发)
|
||||
if (!kIsWeb) {
|
||||
await _requestPermissions();
|
||||
}
|
||||
await _requestPermissions();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
||||
@ -474,7 +474,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
await _addNewBookWithAnimation(
|
||||
title: saveResult['title'] as String? ?? '新故事',
|
||||
content: saveResult['content'] as String? ?? '',
|
||||
coverUrl: saveResult['cover_url'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -602,7 +601,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
_addNewBookWithAnimation(
|
||||
title: saveResult['title'] as String? ?? '新故事',
|
||||
content: saveResult['content'] as String? ?? '',
|
||||
coverUrl: saveResult['cover_url'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -902,7 +900,7 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addNewBookWithAnimation({String title = '新故事', String content = '', String coverUrl = ''}) async {
|
||||
Future<void> _addNewBookWithAnimation({String title = '新故事', String content = ''}) async {
|
||||
// Find the first shelf that has space
|
||||
int? targetShelfId;
|
||||
for (final shelf in _shelves) {
|
||||
@ -925,7 +923,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
'title': title,
|
||||
'content': content,
|
||||
'shelf_id': targetShelfId,
|
||||
if (coverUrl.isNotEmpty) 'cover_url': coverUrl,
|
||||
});
|
||||
// Reload to get the new story
|
||||
await _loadShelves();
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
import '../features/device/presentation/controllers/device_controller.dart';
|
||||
import '../features/device/domain/entities/device.dart';
|
||||
import '../widgets/ios_toast.dart';
|
||||
|
||||
class ProductSelectionPage extends ConsumerStatefulWidget {
|
||||
class ProductSelectionPage extends StatefulWidget {
|
||||
const ProductSelectionPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductSelectionPage> createState() => _ProductSelectionPageState();
|
||||
State<ProductSelectionPage> createState() => _ProductSelectionPageState();
|
||||
}
|
||||
|
||||
class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
class _ProductSelectionPageState extends State<ProductSelectionPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
@ -31,30 +28,12 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 产品 ID 到后端 product_code 的映射
|
||||
static const Map<String, List<String>> _productCodeMap = {
|
||||
'capybara': ['KPBL-ON'],
|
||||
'badge-ai': ['DZBJ-ON'],
|
||||
'badge-basic': ['DZBJ-OFF'],
|
||||
};
|
||||
|
||||
/// 查找用户是否已绑定该产品类型的设备
|
||||
UserDevice? _findBoundDevice(String productId, List<UserDevice> devices) {
|
||||
final codes = _productCodeMap[productId];
|
||||
if (codes == null || codes.isEmpty) return null;
|
||||
for (final device in devices) {
|
||||
final dt = device.device.deviceType ?? device.device.deviceTypeInfo;
|
||||
if (dt != null && codes.contains(dt.productCode)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static final List<Map<String, dynamic>> _products = [
|
||||
{
|
||||
'id': 'capybara',
|
||||
'name': '毛绒机芯',
|
||||
'status': '已连接',
|
||||
'statusColor': const Color(0xFF10B981),
|
||||
'icon': 'assets/www/Capybara.png',
|
||||
'isPng': true,
|
||||
'hasTag': true,
|
||||
@ -66,10 +45,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFFC9A07A),
|
||||
'selected': true,
|
||||
},
|
||||
{
|
||||
'id': 'badge-ai',
|
||||
'name': '电子吧唧 AI',
|
||||
'status': '离线',
|
||||
'statusColor': const Color(0xFFE5E7EB),
|
||||
'icon': 'assets/www/icons/icon-product-badge.svg',
|
||||
'isPng': false,
|
||||
'hasTag': true,
|
||||
@ -81,10 +63,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFF6366F1),
|
||||
'selected': false,
|
||||
},
|
||||
{
|
||||
'id': 'badge-basic',
|
||||
'name': '普通吧唧',
|
||||
'status': '未配对',
|
||||
'statusColor': const Color(0xFFE5E7EB),
|
||||
'icon': 'assets/www/icons/icon-product-badge.svg',
|
||||
'isPng': false,
|
||||
'hasTag': false,
|
||||
@ -95,10 +80,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFFA78BFA),
|
||||
'selected': false,
|
||||
},
|
||||
{
|
||||
'id': 'bracelet',
|
||||
'name': 'AI 手链',
|
||||
'status': '点击扫描',
|
||||
'statusColor': const Color(0xFFE5E7EB),
|
||||
'icon': 'assets/www/icons/icon-product-badge.svg',
|
||||
'isPng': false,
|
||||
'hasTag': true,
|
||||
@ -110,10 +98,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFFE07B54),
|
||||
'selected': false,
|
||||
},
|
||||
{
|
||||
'id': 'vsinger',
|
||||
'name': '洛天依',
|
||||
'status': '去下载专属 APP →',
|
||||
'statusColor': Colors.transparent,
|
||||
'icon': 'assets/www/icons/icon-product-luo.svg',
|
||||
'isPng': false,
|
||||
'hasTag': false,
|
||||
@ -124,10 +115,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFF2DD4BF),
|
||||
'selected': false,
|
||||
},
|
||||
{
|
||||
'id': 'nightlight',
|
||||
'name': 'AI 星空夜灯',
|
||||
'status': '未配对',
|
||||
'statusColor': const Color(0xFFE5E7EB),
|
||||
'icon': 'assets/www/icons/icon-product-badge.svg',
|
||||
'isPng': false,
|
||||
'hasTag': true,
|
||||
@ -139,10 +133,13 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFF7C3AED),
|
||||
'selected': false,
|
||||
},
|
||||
{
|
||||
'id': 'feeder',
|
||||
'name': '智能喂食器',
|
||||
'status': '点击扫描',
|
||||
'statusColor': const Color(0xFFE5E7EB),
|
||||
'icon': 'assets/www/icons/icon-product-badge.svg',
|
||||
'isPng': false,
|
||||
'hasTag': false,
|
||||
@ -153,24 +150,24 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
'shadowColor': const Color(0xFFE11D48),
|
||||
'selected': false,
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final safeTop = MediaQuery.of(context).padding.top;
|
||||
// 头部高度 = safeArea + padding + 按钮高度 + padding
|
||||
final headerHeight = safeTop + 12 + 40 + 12;
|
||||
|
||||
// 获取用户已绑定的设备列表
|
||||
final devicesAsync = ref.watch(deviceControllerProvider);
|
||||
final devices = devicesAsync.value ?? [];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 1. 全屏流动背景
|
||||
const AnimatedGradientBackground(),
|
||||
|
||||
// 2. 列表
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: false,
|
||||
@ -181,28 +178,17 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
itemCount: _products.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final product = _products[index];
|
||||
final boundDevice = _findBoundDevice(product['id'] as String, devices);
|
||||
return _FadeOnScrollCard(
|
||||
key: ValueKey(product['id']),
|
||||
product: product,
|
||||
isBound: boundDevice != null,
|
||||
key: ValueKey(_products[index]['id']),
|
||||
product: _products[index],
|
||||
fadeStartY: headerHeight + 16,
|
||||
fadeEndY: safeTop,
|
||||
onTap: () {
|
||||
if (boundDevice != null) {
|
||||
// 已绑定 → 直接进入设备控制页
|
||||
context.go('/device-control');
|
||||
} else {
|
||||
// 未绑定 → 跳转蓝牙搜索页
|
||||
context.go('/bluetooth');
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 头部 — 统一规范:返回按钮 + 居中标题
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
@ -212,6 +198,7 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 居中标题
|
||||
Text(
|
||||
'选择产品',
|
||||
style: GoogleFonts.outfit(
|
||||
@ -220,6 +207,7 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
color: const Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
// 左侧返回按钮
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: GestureDetector(
|
||||
@ -250,21 +238,17 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 带滚动渐隐的卡片包装器
|
||||
/// 带滚动渐隐的卡片包装器 — 纯 Opacity,无 ShaderMask,无 widget 切换
|
||||
class _FadeOnScrollCard extends StatefulWidget {
|
||||
final Map<String, dynamic> product;
|
||||
final bool isBound;
|
||||
final double fadeStartY;
|
||||
final double fadeEndY;
|
||||
final VoidCallback onTap;
|
||||
final double fadeStartY; // 卡片顶部到此位置开始淡出
|
||||
final double fadeEndY; // 卡片顶部到此位置完全消失
|
||||
|
||||
const _FadeOnScrollCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.isBound,
|
||||
required this.fadeStartY,
|
||||
required this.fadeEndY,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -295,11 +279,7 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> {
|
||||
return Opacity(
|
||||
key: _posKey,
|
||||
opacity: opacity,
|
||||
child: _ProductCard(
|
||||
product: widget.product,
|
||||
isBound: widget.isBound,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
child: _ProductCard(product: widget.product),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -307,20 +287,22 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> {
|
||||
/// 产品卡片
|
||||
class _ProductCard extends StatelessWidget {
|
||||
final Map<String, dynamic> product;
|
||||
final bool isBound;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.isBound,
|
||||
required this.onTap,
|
||||
});
|
||||
const _ProductCard({super.key, required this.product});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isSelected = product['selected'] == true;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
AppToast.show(
|
||||
context, '${product['name']} 离线或未配对', isError: true);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 120,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
@ -361,27 +343,29 @@ class _ProductCard extends StatelessWidget {
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isBound
|
||||
? const Color(0xFF34D399)
|
||||
: Colors.white.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: isBound
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF34D399)
|
||||
.withOpacity(0.4),
|
||||
spreadRadius: 2)
|
||||
]
|
||||
: [],
|
||||
if ((product['statusColor'] as Color) !=
|
||||
Colors.transparent)
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: product['id'] == 'capybara'
|
||||
? const Color(0xFF34D399)
|
||||
: Colors.white.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: product['id'] == 'capybara'
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF34D399)
|
||||
.withOpacity(0.4),
|
||||
spreadRadius: 2)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isBound ? '已连接' : '点击配对',
|
||||
product['status'],
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withOpacity(0.85)),
|
||||
|
||||
@ -3,8 +3,9 @@ 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/features/device/domain/entities/role_memory.dart';
|
||||
import 'package:airhub_app/features/device/presentation/controllers/device_controller.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';
|
||||
|
||||
class AgentManagePage extends ConsumerStatefulWidget {
|
||||
const AgentManagePage({super.key});
|
||||
@ -16,7 +17,7 @@ class AgentManagePage extends ConsumerStatefulWidget {
|
||||
class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final memoriesAsync = ref.watch(roleMemoryControllerProvider);
|
||||
final spiritsAsync = ref.watch(spiritControllerProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -27,7 +28,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Expanded(
|
||||
child: memoriesAsync.when(
|
||||
child: spiritsAsync.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
@ -44,7 +45,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () => ref.read(roleMemoryControllerProvider.notifier).refresh(),
|
||||
onTap: () => ref.read(spiritControllerProvider.notifier).refresh(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
@ -60,8 +61,8 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (memories) {
|
||||
if (memories.isEmpty) {
|
||||
data: (spirits) {
|
||||
if (spirits.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'暂无角色记忆',
|
||||
@ -79,9 +80,9 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
right: 20,
|
||||
bottom: 40 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemCount: memories.length,
|
||||
itemCount: spirits.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMemoryCard(memories[index]);
|
||||
return _buildAgentCard(spirits[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -158,16 +159,16 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMemoryCard(RoleMemory memory) {
|
||||
final dateStr = memory.createdAt != null
|
||||
? memory.createdAt!.substring(0, 10).replaceAll('-', '/')
|
||||
Widget _buildAgentCard(Spirit spirit) {
|
||||
final dateStr = spirit.createdAt != null
|
||||
? spirit.createdAt!.substring(0, 10).replaceAll('-', '/')
|
||||
: '';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD4A373),
|
||||
color: const Color(0xFFD4A373), // Fallback
|
||||
gradient: const LinearGradient(colors: AppColors.gradientCapybara),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
@ -180,6 +181,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Top highlight layer
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
@ -224,7 +226,19 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text('🧠', style: TextStyle(fontSize: 24)),
|
||||
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)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@ -232,9 +246,7 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
memory.deviceTypeName.isNotEmpty
|
||||
? memory.deviceTypeName
|
||||
: '角色记忆 #${memory.id}',
|
||||
spirit.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -248,36 +260,32 @@ 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),
|
||||
_buildDetailRow('状态:', memory.isBound ? '已绑定' : '空闲'),
|
||||
if (memory.memorySummary.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
_buildDetailRow('记忆:', memory.memorySummary.length > 30
|
||||
? '${memory.memorySummary.substring(0, 30)}...'
|
||||
: memory.memorySummary),
|
||||
],
|
||||
_buildDetailRow('状态:', spirit.isActive ? '活跃' : '未激活'),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Container(height: 1, color: Colors.white.withOpacity(0.2)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_buildStatusTag(memory.isBound),
|
||||
_buildActionBtn(
|
||||
'解绑',
|
||||
isDanger: true,
|
||||
onTap: () => _showUnbindDialog(spirit),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionBtn(
|
||||
'删除',
|
||||
isDanger: true,
|
||||
onTap: () => _showDeleteDialog(spirit),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -305,33 +313,84 @@ class _AgentManagePageState extends ConsumerState<AgentManagePage> {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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}' : '删除失败');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ 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});
|
||||
@ -27,11 +26,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
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(
|
||||
@ -54,8 +48,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
_buildItem(
|
||||
'📱',
|
||||
'绑定手机',
|
||||
value: maskedPhone.isNotEmpty ? maskedPhone : '未绑定',
|
||||
onTap: () => _showMessage('绑定手机', maskedPhone.isNotEmpty ? maskedPhone : '未绑定'),
|
||||
value: '138****3069',
|
||||
onTap: () => _showMessage('绑定手机', '138****3069'),
|
||||
),
|
||||
_buildItem(
|
||||
'🔐',
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'product_selection_page.dart';
|
||||
import '../widgets/glass_dialog.dart';
|
||||
import '../widgets/animated_gradient_background.dart';
|
||||
import '../widgets/ios_toast.dart';
|
||||
@ -493,7 +494,10 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
.unbindDevice(_userDeviceId!);
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
context.go('/product-selection');
|
||||
Navigator.pop(context); // close settings
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const ProductSelectionPage()),
|
||||
);
|
||||
} else {
|
||||
AppToast.show(context, '解绑失败', isError: true);
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show PlatformException;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import '../widgets/gradient_button.dart';
|
||||
import '../widgets/pill_progress_button.dart';
|
||||
@ -35,10 +34,6 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
bool _hasGeneratedVideo = false;
|
||||
bool _isLoadingVideo = false;
|
||||
|
||||
// Video Player
|
||||
VideoPlayerController? _videoController;
|
||||
bool _videoInitialized = false;
|
||||
|
||||
// TTS — uses global TTSService singleton
|
||||
final TTSService _ttsService = TTSService.instance;
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
@ -113,15 +108,6 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
debugPrint('durationStream error (ignored): $e');
|
||||
});
|
||||
|
||||
// Auto-show video tab if story already has a video
|
||||
final hasVideo = _currentStory['has_video'] == true;
|
||||
final videoUrl = _currentStory['video_url'] as String? ?? '';
|
||||
if (hasVideo && videoUrl.isNotEmpty) {
|
||||
_hasGeneratedVideo = true;
|
||||
_activeTab = 'video';
|
||||
_initVideoPlayer(videoUrl);
|
||||
}
|
||||
|
||||
// Check if audio already exists
|
||||
debugPrint('[StoryDetail] story keys: ${_currentStory.keys.toList()}');
|
||||
debugPrint('[StoryDetail] audio_url value: "${_currentStory['audio_url']}"');
|
||||
@ -165,33 +151,12 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _initVideoPlayer(String url) async {
|
||||
try {
|
||||
final controller = VideoPlayerController.networkUrl(Uri.parse(url));
|
||||
_videoController = controller;
|
||||
controller.addListener(_onVideoChanged);
|
||||
await controller.initialize();
|
||||
if (mounted) {
|
||||
setState(() => _videoInitialized = true);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Video init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onVideoChanged() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ttsService.removeListener(_onTTSChanged);
|
||||
_positionSub?.cancel();
|
||||
_playerStateSub?.cancel();
|
||||
_audioPlayer.dispose();
|
||||
_videoController?.removeListener(_onVideoChanged);
|
||||
_videoController?.dispose();
|
||||
_genieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -206,8 +171,7 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
|
||||
if (_ttsService.error != null &&
|
||||
!_ttsService.isGenerating &&
|
||||
_ttsService.audioUrl == null &&
|
||||
_ttsService.errorTitle == title) {
|
||||
_ttsService.audioUrl == null) {
|
||||
return TTSButtonState.error;
|
||||
}
|
||||
if (_ttsService.isGeneratingFor(title)) {
|
||||
@ -554,58 +518,28 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
||||
);
|
||||
}
|
||||
|
||||
// Not yet initialized — black + spinner while video loads
|
||||
if (!_videoInitialized || _videoController == null) {
|
||||
return const AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: Colors.white54, strokeWidth: 3),
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: const Center(
|
||||
child: Icon(Icons.videocam, color: Colors.white54, size: 48),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Initialized: VideoPlayer shows frame 0 naturally as thumbnail when paused
|
||||
final isPlaying = _videoController!.value.isPlaying;
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: _videoController!.value.aspectRatio,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Video fills the area (Positioned.fill avoids StackFit.expand distortion)
|
||||
Positioned.fill(child: VideoPlayer(_videoController!)),
|
||||
// Full-area tap handler
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
if (_videoController!.value.isPlaying) {
|
||||
await _videoController!.pause();
|
||||
} else {
|
||||
await _videoController!.play();
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
// Play button — IgnorePointer lets taps pass through to GestureDetector below
|
||||
if (!isPlaying)
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.play_arrow, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.play_arrow, color: Colors.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
|
||||
String buffer = '';
|
||||
String? storyTitle;
|
||||
String? storyContent;
|
||||
String storyCoverUrl = '';
|
||||
|
||||
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
||||
buffer += chunk;
|
||||
@ -110,13 +109,9 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
|
||||
case 'parsing':
|
||||
_updateProgress(progress / 100, '正在编制最后的魔法...');
|
||||
break;
|
||||
case 'cover':
|
||||
_updateProgress(progress / 100, '正在绘制故事封面...');
|
||||
break;
|
||||
case 'done':
|
||||
storyTitle = event['title'] as String? ?? '卡皮巴拉的故事';
|
||||
storyContent = event['content'] as String? ?? '';
|
||||
storyCoverUrl = event['cover_url'] as String? ?? '';
|
||||
_updateProgress(1.0, '大功告成!');
|
||||
break;
|
||||
case 'error':
|
||||
@ -147,7 +142,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
|
||||
story: {
|
||||
'title': storyTitle,
|
||||
'content': storyContent,
|
||||
'cover_url': storyCoverUrl,
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -160,7 +154,6 @@ class _StoryLoadingPageState extends State<StoryLoadingPage> {
|
||||
'action': 'saved',
|
||||
'title': storyTitle,
|
||||
'content': storyContent,
|
||||
'cover_url': storyCoverUrl,
|
||||
});
|
||||
} else {
|
||||
Navigator.of(context).pop(result);
|
||||
|
||||
@ -29,7 +29,6 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
double _progress = 0.0;
|
||||
String _progressText = '正在连接WiFi...';
|
||||
bool _connectFailed = false;
|
||||
bool _isBinding = false;
|
||||
|
||||
// Device Info
|
||||
Map<String, dynamic> _deviceInfo = {};
|
||||
@ -129,20 +128,12 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
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) {
|
||||
try {
|
||||
debugPrint('[WiFi Config] Binding device sn=$sn');
|
||||
await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
|
||||
} catch (e) {
|
||||
debugPrint('[WiFi Config] bindDevice 异常: $e');
|
||||
}
|
||||
debugPrint('[WiFi Config] Binding device sn=$sn');
|
||||
await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _isBinding = false);
|
||||
context.go('/device-control');
|
||||
return;
|
||||
}
|
||||
@ -714,7 +705,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
}
|
||||
if (_currentStep == 4) {
|
||||
showNext = true;
|
||||
nextText = _isBinding ? '绑定中...' : '进入设备';
|
||||
nextText = '进入设备';
|
||||
}
|
||||
|
||||
if (!showNext && _currentStep != 3) {
|
||||
@ -773,7 +764,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
if (_currentStep < 4) const SizedBox(width: 16),
|
||||
GradientButton(
|
||||
text: nextText,
|
||||
onPressed: _isBinding ? null : _handleNext,
|
||||
onPressed: _handleNext,
|
||||
height: 56,
|
||||
width: _currentStep == 4 ? 200 : 160,
|
||||
),
|
||||
|
||||
@ -29,7 +29,6 @@ class TTSService extends ChangeNotifier {
|
||||
|
||||
// ── Error ──
|
||||
String? _error;
|
||||
String? _errorTitle; // Which story the error belongs to
|
||||
|
||||
// ── Getters ──
|
||||
bool get isGenerating => _isGenerating;
|
||||
@ -40,7 +39,6 @@ class TTSService extends ChangeNotifier {
|
||||
String? get completedStoryTitle => _completedStoryTitle;
|
||||
bool get justCompleted => _justCompleted;
|
||||
String? get error => _error;
|
||||
String? get errorTitle => _errorTitle;
|
||||
|
||||
/// Check if audio is ready for a specific story.
|
||||
bool hasAudioFor(String title) {
|
||||
@ -184,7 +182,6 @@ class TTSService extends ChangeNotifier {
|
||||
_isGenerating = false;
|
||||
if (_audioUrl == null) {
|
||||
_error = '未获取到音频';
|
||||
_errorTitle = title;
|
||||
_statusMessage = '生成失败';
|
||||
}
|
||||
notifyListeners();
|
||||
@ -193,7 +190,6 @@ class TTSService extends ChangeNotifier {
|
||||
_isGenerating = false;
|
||||
_progress = 0.0;
|
||||
_error = e.toString();
|
||||
_errorTitle = title;
|
||||
_statusMessage = '生成失败';
|
||||
_justCompleted = false;
|
||||
notifyListeners();
|
||||
@ -216,7 +212,6 @@ class TTSService extends ChangeNotifier {
|
||||
_completedStoryTitle = null;
|
||||
_justCompleted = false;
|
||||
_error = null;
|
||||
_errorTitle = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import file_selector_macos
|
||||
import flutter_blue_plus_darwin
|
||||
import just_audio
|
||||
import shared_preferences_foundation
|
||||
import video_player_avfoundation
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@ -19,6 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
||||
@ -1,38 +1,57 @@
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'dart:html' as html show window;
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
import 'ali_auth_web_api.dart';
|
||||
import 'ali_auth_platform_interface.dart';
|
||||
export 'ali_auth_method_channel.dart';
|
||||
|
||||
/// Web 平台 stub —— 一键登录依赖运营商 SIM 卡,Web 上不可用。
|
||||
/// 所有方法均为空实现,PhoneAuthService 已通过 kIsWeb 守卫跳过调用。
|
||||
/// A web implementation of the AliAuthPlatform of the AliAuth plugin.
|
||||
class AliAuthPluginWeb extends AliAuthPlatform {
|
||||
/// Constructs a AliAuthWeb
|
||||
AliAuthPluginWeb();
|
||||
|
||||
AliAuthPluginWebApi aliAuthPluginWebApi = AliAuthPluginWebApi();
|
||||
|
||||
static void registerWith(Registrar registrar) {
|
||||
AliAuthPlatform.instance = AliAuthPluginWeb();
|
||||
}
|
||||
|
||||
/// Returns a [String] containing the version of the platform.
|
||||
@override
|
||||
Future<String?> getPlatformVersion() async => 'web (unsupported)';
|
||||
Future<String?> getPlatformVersion() async {
|
||||
final version = html.window.navigator.userAgent;
|
||||
return version;
|
||||
}
|
||||
|
||||
/// 获取SDK版本号
|
||||
@override
|
||||
Future<String?> getSdkVersion() async => null;
|
||||
Future<String?> getSdkVersion() async {
|
||||
return await aliAuthPluginWebApi.getVersion();
|
||||
}
|
||||
|
||||
/// 网络类型检查接口
|
||||
@override
|
||||
Future<String?> getConnection() async => null;
|
||||
Future<String?> getConnection() async {
|
||||
return await aliAuthPluginWebApi.getConnection();
|
||||
}
|
||||
|
||||
/// 设置SDK是否开启日志。开启后会在控制台打印更多内容便于排查问题。
|
||||
@override
|
||||
Future<void> setLoggerEnable(bool isEnable) async {}
|
||||
Future<void> setLoggerEnable(bool isEnable) async {
|
||||
return await aliAuthPluginWebApi.setLoggerEnable(isEnable);
|
||||
}
|
||||
|
||||
/// 身份鉴权
|
||||
@override
|
||||
Future<void> checkAuthAvailable(String accessToken, String jwtToken,
|
||||
Function(dynamic) success, Function(dynamic) error) async {
|
||||
debugPrint('[AliAuth] Web 平台不支持一键登录');
|
||||
aliAuthPluginWebApi.checkAuthAvailable(
|
||||
accessToken, jwtToken, success, error);
|
||||
}
|
||||
|
||||
/// 获取本机号码校验Token
|
||||
@override
|
||||
Future<void> getVerifyToken(
|
||||
Function(dynamic) success, Function(dynamic) error) async {
|
||||
debugPrint('[AliAuth] Web 平台不支持一键登录');
|
||||
aliAuthPluginWebApi.getVerifyToken(success, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,10 +132,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -232,14 +232,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
custom_lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -548,14 +540,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -660,6 +644,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -744,18 +736,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.18"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1213,26 +1205,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
|
||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.29.0"
|
||||
version: "1.26.3"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
version: "0.7.7"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
|
||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.15"
|
||||
version: "0.6.12"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1281,46 +1273,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "08bfba72e311d48219acad4e191b1f9c27ff8cf928f2c7234874592d9c9d7341"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "9862c67c4661c98f30fe707bc1a4f97d6a0faa76784f485d282668e4651a7ac3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.3"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -67,7 +67,6 @@ dependencies:
|
||||
image_picker: ^1.2.1
|
||||
just_audio: ^0.9.42
|
||||
http: ^1.2.0
|
||||
video_player: ^2.9.2
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
#!/bin/bash
|
||||
# 重新编译并启动 Flutter Web (localhost:8080)
|
||||
|
||||
# 杀掉占用 8080 端口的进程(强制)
|
||||
# 杀掉占用 8080 端口的进程
|
||||
PID=$(lsof -ti:8080 2>/dev/null)
|
||||
if [ -n "$PID" ]; then
|
||||
echo "正在强制停止旧进程 (PID: $PID)..."
|
||||
kill -9 $PID 2>/dev/null
|
||||
# 等待端口真正释放(最多 5 秒)
|
||||
for i in $(seq 1 10); do
|
||||
lsof -ti:8080 > /dev/null 2>&1 || break
|
||||
sleep 0.5
|
||||
done
|
||||
echo "正在停止旧进程 (PID: $PID)..."
|
||||
kill $PID 2>/dev/null
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
echo "正在编译并启动 Flutter Web..."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user