diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c4ad5e2 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(flutter doctor:*)", + "Bash(flutter devices:*)", + "Bash(flutter pub get:*)", + "Bash(pod install)", + "Bash(flutter run:*)" + ] + } +} diff --git a/airhub_app/.metadata b/airhub_app/.metadata index 79e4428..d73f86e 100644 --- a/airhub_app/.metadata +++ b/airhub_app/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "67323de285b00232883f53b84095eb72be97d35c" + revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46" channel: "stable" project_type: app @@ -13,17 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - platform: android - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - platform: ios - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c - - platform: web - create_revision: 67323de285b00232883f53b84095eb72be97d35c - base_revision: 67323de285b00232883f53b84095eb72be97d35c + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: macos + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 # User provided section diff --git a/airhub_app/android/app/src/main/AndroidManifest.xml b/airhub_app/android/app/src/main/AndroidManifest.xml index 00b6227..1c33bdf 100644 --- a/airhub_app/android/app/src/main/AndroidManifest.xml +++ b/airhub_app/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,11 @@ + + + + + + + UIApplicationSupportsIndirectInputEvents + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限来搜索和连接您的设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限来搜索和连接您的设备 + NSLocationWhenInUseUsageDescription + 需要位置权限以扫描附近的蓝牙设备 UILaunchScreen UIColorName diff --git a/airhub_app/lib/core/errors/exceptions.dart b/airhub_app/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..9ebeabe --- /dev/null +++ b/airhub_app/lib/core/errors/exceptions.dart @@ -0,0 +1,20 @@ +/// 服务端业务异常(后端返回 code != 0) +class ServerException implements Exception { + final int code; + final String message; + + const ServerException(this.code, this.message); + + @override + String toString() => message; +} + +/// 网络异常(无法连接服务器) +class NetworkException implements Exception { + final String message; + + const NetworkException([this.message = '网络连接失败,请检查网络']); + + @override + String toString() => message; +} diff --git a/airhub_app/lib/core/network/api_client.dart b/airhub_app/lib/core/network/api_client.dart new file mode 100644 index 0000000..a37fafc --- /dev/null +++ b/airhub_app/lib/core/network/api_client.dart @@ -0,0 +1,226 @@ +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../errors/exceptions.dart'; +import '../services/log_center_service.dart'; +import 'api_config.dart'; +import 'token_manager.dart'; + +part 'api_client.g.dart'; + +@Riverpod(keepAlive: true) +ApiClient apiClient(Ref ref) { + final tokenManager = ref.watch(tokenManagerProvider); + final logCenter = ref.watch(logCenterServiceProvider); + return ApiClient(tokenManager, logCenter); +} + +class ApiClient { + final TokenManager _tokenManager; + final LogCenterService _logCenter; + late final Dio _dio; + + ApiClient(this._tokenManager, this._logCenter) { + _dio = Dio(BaseOptions( + baseUrl: ApiConfig.fullBaseUrl, + connectTimeout: ApiConfig.connectTimeout, + receiveTimeout: ApiConfig.receiveTimeout, + headers: {'Content-Type': 'application/json'}, + )); + + _dio.interceptors.add(_AuthInterceptor(_tokenManager, _dio)); + _dio.interceptors.add(_LogCenterInterceptor(_logCenter)); + } + + /// GET 请求,返回 data 字段 + Future get( + String path, { + Map? queryParameters, + }) async { + final response = await _request( + () => _dio.get(path, queryParameters: queryParameters), + ); + return response; + } + + /// POST 请求,返回 data 字段 + Future post( + String path, { + dynamic data, + }) async { + final response = await _request( + () => _dio.post(path, data: data), + ); + return response; + } + + /// PUT 请求,返回 data 字段 + Future put( + String path, { + dynamic data, + }) async { + final response = await _request( + () => _dio.put(path, data: data), + ); + return response; + } + + /// DELETE 请求,返回 data 字段 + Future delete( + String path, { + dynamic data, + }) async { + final response = await _request( + () => _dio.delete(path, data: data), + ); + return response; + } + + /// 统一请求处理:解析后端 {code, message, data} 格式 + Future _request(Future Function() request) async { + try { + final response = await request(); + final body = response.data; + + if (body is Map) { + final code = body['code'] as int? ?? -1; + final message = body['message'] as String? ?? '未知错误'; + final data = body['data']; + + if (code == 0) { + return data; + } else { + throw ServerException(code, message); + } + } + + return body; + } on DioException catch (e) { + if (e.response != null) { + final body = e.response?.data; + if (body is Map) { + final code = body['code'] as int? ?? -1; + final message = body['message'] as String? ?? '请求失败'; + throw ServerException(code, message); + } + } + throw const NetworkException(); + } + } +} + +/// Auth 拦截器:自动附加 Bearer Token +class _AuthInterceptor extends Interceptor { + final TokenManager _tokenManager; + final Dio _dio; + + _AuthInterceptor(this._tokenManager, this._dio); + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + // 不需要 token 的路径 + const noAuthPaths = [ + '/auth/send-code/', + '/auth/code-login/', + '/auth/phone-login/', + '/auth/refresh/', + '/version/check/', + '/devices/query-by-mac/', + ]; + + final needsAuth = !noAuthPaths.any((p) => options.path.contains(p)); + + if (needsAuth) { + final token = await _tokenManager.getAccessToken(); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + } + + handler.next(options); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + // 401: 尝试 refresh token + if (err.response?.statusCode == 401) { + try { + final refreshToken = await _tokenManager.getRefreshToken(); + if (refreshToken != null) { + final response = await _dio.post( + '/auth/refresh/', + data: {'refresh': refreshToken}, + options: Options(headers: {'Authorization': ''}), + ); + + final body = response.data; + if (body is Map && body['code'] == 0) { + final data = body['data'] as Map; + await _tokenManager.saveTokens( + access: data['access'] as String, + refresh: data['refresh'] as String, + ); + + // 用新 token 重试原请求 + final opts = err.requestOptions; + opts.headers['Authorization'] = + 'Bearer ${data['access']}'; + final retryResponse = await _dio.fetch(opts); + return handler.resolve(retryResponse); + } + } + } catch (_) { + // refresh 失败,清除 token(会触发路由守卫跳转登录) + await _tokenManager.clearTokens(); + } + } + + handler.next(err); + } +} + +/// Log Center 拦截器:自动上报 API 错误 +class _LogCenterInterceptor extends Interceptor { + final LogCenterService _logCenter; + + _LogCenterInterceptor(this._logCenter); + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + final req = err.requestOptions; + final statusCode = err.response?.statusCode; + + // 跳过 401(token 过期是正常流程,不上报) + if (statusCode == 401) { + handler.next(err); + return; + } + + // 提取后端返回的业务错误信息 + String errorMessage = err.message ?? err.type.name; + String errorType = 'DioException.${err.type.name}'; + final body = err.response?.data; + if (body is Map) { + errorType = 'ApiError(${body['code'] ?? statusCode})'; + errorMessage = body['message'] as String? ?? errorMessage; + } + + _logCenter.reportError( + Exception(errorMessage), + context: { + 'type': 'api_error', + 'error_type': errorType, + 'url': '${req.method} ${req.path}', + 'status_code': statusCode, + 'query': req.queryParameters.isNotEmpty + ? req.queryParameters.toString() + : null, + }, + ); + + handler.next(err); + } +} diff --git a/airhub_app/lib/core/network/api_client.g.dart b/airhub_app/lib/core/network/api_client.g.dart new file mode 100644 index 0000000..2e0dd3c --- /dev/null +++ b/airhub_app/lib/core/network/api_client.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_client.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(apiClient) +const apiClientProvider = ApiClientProvider._(); + +final class ApiClientProvider + extends $FunctionalProvider + with $Provider { + const ApiClientProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'apiClientProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$apiClientHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + ApiClient create(Ref ref) { + return apiClient(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ApiClient value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$apiClientHash() => r'03fa482085a0f74d1526b1a511e1b3c555269918'; diff --git a/airhub_app/lib/core/network/api_config.dart b/airhub_app/lib/core/network/api_config.dart new file mode 100644 index 0000000..89b6863 --- /dev/null +++ b/airhub_app/lib/core/network/api_config.dart @@ -0,0 +1,16 @@ +class ApiConfig { + /// 后端服务器地址(开发环境请替换为实际 IP) + static const String baseUrl = 'http://192.168.124.24:8000'; + + /// App 端 API 前缀 + static const String apiPrefix = '/api/v1'; + + /// 连接超时 + static const Duration connectTimeout = Duration(seconds: 15); + + /// 接收超时 + static const Duration receiveTimeout = Duration(seconds: 15); + + /// 拼接完整 URL + static String get fullBaseUrl => '$baseUrl$apiPrefix'; +} diff --git a/airhub_app/lib/core/network/token_manager.dart b/airhub_app/lib/core/network/token_manager.dart new file mode 100644 index 0000000..19e7e41 --- /dev/null +++ b/airhub_app/lib/core/network/token_manager.dart @@ -0,0 +1,54 @@ +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'token_manager.g.dart'; + +const _keyAccessToken = 'access_token'; +const _keyRefreshToken = 'refresh_token'; + +@Riverpod(keepAlive: true) +TokenManager tokenManager(Ref ref) { + return TokenManager(); +} + +class TokenManager extends ChangeNotifier { + SharedPreferences? _prefs; + + Future get _preferences async { + _prefs ??= await SharedPreferences.getInstance(); + return _prefs!; + } + + Future saveTokens({ + required String access, + required String refresh, + }) async { + final prefs = await _preferences; + await prefs.setString(_keyAccessToken, access); + await prefs.setString(_keyRefreshToken, refresh); + notifyListeners(); + } + + Future getAccessToken() async { + final prefs = await _preferences; + return prefs.getString(_keyAccessToken); + } + + Future getRefreshToken() async { + final prefs = await _preferences; + return prefs.getString(_keyRefreshToken); + } + + Future hasToken() async { + final token = await getAccessToken(); + return token != null && token.isNotEmpty; + } + + Future clearTokens() async { + final prefs = await _preferences; + await prefs.remove(_keyAccessToken); + await prefs.remove(_keyRefreshToken); + notifyListeners(); + } +} diff --git a/airhub_app/lib/core/network/token_manager.g.dart b/airhub_app/lib/core/network/token_manager.g.dart new file mode 100644 index 0000000..dfc91b3 --- /dev/null +++ b/airhub_app/lib/core/network/token_manager.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'token_manager.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(tokenManager) +const tokenManagerProvider = TokenManagerProvider._(); + +final class TokenManagerProvider + extends $FunctionalProvider + with $Provider { + const TokenManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'tokenManagerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$tokenManagerHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + TokenManager create(Ref ref) { + return tokenManager(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(TokenManager value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$tokenManagerHash() => r'94bb9e39530e1d18331ea750bd4b6c5d4f16f1e9'; diff --git a/airhub_app/lib/core/router/app_router.dart b/airhub_app/lib/core/router/app_router.dart index 6b7c8d5..fe4a7df 100644 --- a/airhub_app/lib/core/router/app_router.dart +++ b/airhub_app/lib/core/router/app_router.dart @@ -8,14 +8,29 @@ import '../../pages/home_page.dart'; import '../../pages/profile/profile_page.dart'; import '../../pages/webview_page.dart'; import '../../pages/wifi_config_page.dart'; +import '../network/token_manager.dart'; part 'app_router.g.dart'; @riverpod GoRouter goRouter(Ref ref) { + final tokenManager = ref.watch(tokenManagerProvider); + return GoRouter( - initialLocation: - '/login', // Start at login for now, logic can be added to check auth state later + initialLocation: '/login', + refreshListenable: tokenManager, + redirect: (context, state) async { + final hasToken = await tokenManager.hasToken(); + final isLoginRoute = state.matchedLocation == '/login'; + + if (!hasToken && !isLoginRoute) { + return '/login'; + } + if (hasToken && isLoginRoute) { + return '/home'; + } + return null; + }, routes: [ GoRoute(path: '/login', builder: (context, state) => const LoginPage()), GoRoute(path: '/home', builder: (context, state) => const HomePage()), @@ -29,7 +44,9 @@ GoRouter goRouter(Ref ref) { ), GoRoute( path: '/wifi-config', - builder: (context, state) => const WifiConfigPage(), + builder: (context, state) => WifiConfigPage( + extra: state.extra as Map?, + ), ), GoRoute( path: '/device-control', diff --git a/airhub_app/lib/core/router/app_router.g.dart b/airhub_app/lib/core/router/app_router.g.dart index 16ea3e2..c2f3d9a 100644 --- a/airhub_app/lib/core/router/app_router.g.dart +++ b/airhub_app/lib/core/router/app_router.g.dart @@ -48,4 +48,4 @@ final class GoRouterProvider } } -String _$goRouterHash() => r'937320fb6893b1da17afec22844ae01cf2e22441'; +String _$goRouterHash() => r'8e620e452bb81f2c6ed87b136283a9e508dca2e9'; diff --git a/airhub_app/lib/core/services/ble_provisioning_service.dart b/airhub_app/lib/core/services/ble_provisioning_service.dart new file mode 100644 index 0000000..dc28bef --- /dev/null +++ b/airhub_app/lib/core/services/ble_provisioning_service.dart @@ -0,0 +1,282 @@ +import 'dart:async'; +import 'dart:convert' show utf8; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart' show debugPrint; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; + +/// 硬件 BLE 配网协议常量 +class _ProvCmd { + static const int setSsid = 0x01; + static const int setPassword = 0x02; + static const int connectAp = 0x04; + static const int getWifiList = 0x06; +} + +class _ProvResp { + static const int wifiStatus = 0x81; + static const int wifiList = 0x82; + static const int wifiListEnd = 0x83; + static const int customData = 0x84; +} + +/// 配网服务 UUID(与硬件一致) +class _ProvUuid { + static final service = Guid('0000abf0-0000-1000-8000-00805f9b34fb'); + static final writeChar = Guid('0000abf1-0000-1000-8000-00805f9b34fb'); + static final notifyChar = Guid('0000abf2-0000-1000-8000-00805f9b34fb'); +} + +/// 扫描到的 WiFi 网络 +class ScannedWifi { + final String ssid; + final int rssi; + + const ScannedWifi({required this.ssid, required this.rssi}); + + /// 信号强度等级 1-4 + int get level { + if (rssi >= -50) return 4; + if (rssi >= -65) return 3; + if (rssi >= -80) return 2; + return 1; + } +} + +/// WiFi 连接结果 +class WifiResult { + final bool success; + final int reasonCode; + final String? staMac; + + const WifiResult({required this.success, this.reasonCode = 0, this.staMac}); +} + +/// BLE WiFi 配网服务 +/// +/// 封装与硬件的 BLE 通信协议,提供: +/// - 连接 BLE 设备 +/// - 获取 WiFi 列表 +/// - 发送 WiFi 凭证 +/// - 监听连接状态 +class BleProvisioningService { + BluetoothDevice? _device; + BluetoothCharacteristic? _writeChar; + BluetoothCharacteristic? _notifyChar; + StreamSubscription? _notifySubscription; + StreamSubscription? _connectionSubscription; + + bool _connected = false; + bool get isConnected => _connected; + String? get deviceId => _device?.remoteId.str; + + /// 用于传递 WiFi 扫描结果 + final _wifiListController = StreamController>.broadcast(); + Stream> get onWifiList => _wifiListController.stream; + + /// 用于传递 WiFi 连接状态 + final _wifiStatusController = StreamController.broadcast(); + Stream get onWifiStatus => _wifiStatusController.stream; + + /// 用于传递连接断开事件 + final _disconnectController = StreamController.broadcast(); + Stream get onDisconnect => _disconnectController.stream; + + /// 临时存储 WiFi 列表条目 + List _pendingWifiList = []; + + /// 连接到 BLE 设备并发现配网服务 + Future connect(BluetoothDevice device) async { + try { + _device = device; + debugPrint('[BLE Prov] 连接设备: ${device.remoteId}'); + + await device.connect(timeout: const Duration(seconds: 15)); + _connected = true; + debugPrint('[BLE Prov] BLE 连接成功'); + + // 监听连接状态 + _connectionSubscription = device.connectionState.listen((state) { + debugPrint('[BLE Prov] 连接状态变化: $state'); + if (state == BluetoothConnectionState.disconnected) { + debugPrint('[BLE Prov] 设备已断开'); + _connected = false; + _disconnectController.add(null); + } + }); + + // 请求更大的 MTU(iOS 自动协商,可能不支持显式请求) + try { + final mtu = await device.requestMtu(512); + debugPrint('[BLE Prov] MTU 协商成功: $mtu'); + } catch (e) { + debugPrint('[BLE Prov] MTU 协商失败(可忽略): $e'); + } + + // 发现服务 + debugPrint('[BLE Prov] 开始发现服务...'); + final services = await device.discoverServices(); + debugPrint('[BLE Prov] 发现 ${services.length} 个服务'); + + BluetoothService? provService; + for (final s in services) { + debugPrint('[BLE Prov] 服务: ${s.uuid}'); + if (s.uuid == _ProvUuid.service) { + provService = s; + } + } + + if (provService == null) { + debugPrint('[BLE Prov] 未找到配网服务 ${_ProvUuid.service}'); + await disconnect(); + return false; + } + debugPrint('[BLE Prov] 找到配网服务 ABF0'); + + // 找到读写特征 + for (final c in provService.characteristics) { + debugPrint('[BLE Prov] 特征: ${c.uuid}, props: ${c.properties}'); + if (c.uuid == _ProvUuid.writeChar) _writeChar = c; + if (c.uuid == _ProvUuid.notifyChar) _notifyChar = c; + } + + if (_writeChar == null || _notifyChar == null) { + debugPrint('[BLE Prov] 未找到所需特征 writeChar=$_writeChar notifyChar=$_notifyChar'); + await disconnect(); + return false; + } + debugPrint('[BLE Prov] 找到 ABF1(write) + ABF2(notify)'); + + // 订阅 Notify + await _notifyChar!.setNotifyValue(true); + _notifySubscription = _notifyChar!.onValueReceived.listen(_handleNotify); + + debugPrint('[BLE Prov] 配网服务就绪'); + return true; + } catch (e, stack) { + debugPrint('[BLE Prov] 连接失败: $e'); + debugPrint('[BLE Prov] 堆栈: $stack'); + _connected = false; + return false; + } + } + + /// 请求设备扫描 WiFi 网络 + Future requestWifiScan() async { + _pendingWifiList = []; + await _write([_ProvCmd.getWifiList]); + debugPrint('[BLE Prov] 已发送 WiFi 扫描命令'); + } + + /// 发送 WiFi 凭证并触发连接 + Future sendWifiCredentials(String ssid, String password) async { + // 1. 发送 SSID + final ssidBytes = Uint8List.fromList([_ProvCmd.setSsid, ...ssid.codeUnits]); + await _write(ssidBytes); + debugPrint('[BLE Prov] 已发送 SSID: $ssid'); + + // 稍等确保硬件处理完成 + await Future.delayed(const Duration(milliseconds: 100)); + + // 2. 发送密码(硬件收到密码后自动开始连接) + final pwdBytes = Uint8List.fromList([_ProvCmd.setPassword, ...password.codeUnits]); + await _write(pwdBytes); + debugPrint('[BLE Prov] 已发送密码,等待硬件连接 WiFi...'); + } + + /// 断开连接 + Future disconnect() async { + _notifySubscription?.cancel(); + _connectionSubscription?.cancel(); + try { + await _device?.disconnect(); + } catch (_) {} + _connected = false; + debugPrint('[BLE Prov] 已断开'); + } + + /// 释放资源 + void dispose() { + disconnect(); + _wifiListController.close(); + _wifiStatusController.close(); + _disconnectController.close(); + } + + /// 写入数据到 Write 特征 + Future _write(List data) async { + if (_writeChar == null) { + debugPrint('[BLE Prov] writeChar 未就绪'); + return; + } + await _writeChar!.write(data, withoutResponse: false); + } + + /// 处理 Notify 数据 + void _handleNotify(List data) { + if (data.isEmpty) return; + final cmd = data[0]; + debugPrint('[BLE Prov] 收到通知: cmd=0x${cmd.toRadixString(16)}, len=${data.length}'); + + switch (cmd) { + case _ProvResp.wifiList: + _handleWifiListEntry(data); + break; + case _ProvResp.wifiListEnd: + _handleWifiListEnd(); + break; + case _ProvResp.wifiStatus: + _handleWifiStatus(data); + break; + case _ProvResp.customData: + _handleCustomData(data); + break; + } + } + + /// 解析单条 WiFi 列表: [0x82][RSSI][SSID_LEN][SSID...] + void _handleWifiListEntry(List data) { + if (data.length < 4) return; + final rssi = data[1].toSigned(8); // signed byte + final ssidLen = data[2]; + if (data.length < 3 + ssidLen) return; + final ssid = utf8.decode(data.sublist(3, 3 + ssidLen), allowMalformed: true); + if (ssid.isNotEmpty) { + _pendingWifiList.add(ScannedWifi(ssid: ssid, rssi: rssi)); + debugPrint('[BLE Prov] WiFi: $ssid (RSSI: $rssi)'); + } + } + + /// WiFi 列表结束 + void _handleWifiListEnd() { + debugPrint('[BLE Prov] WiFi 列表完成,共 ${_pendingWifiList.length} 个'); + // 按信号强度排序 + _pendingWifiList.sort((a, b) => b.rssi.compareTo(a.rssi)); + _wifiListController.add(List.unmodifiable(_pendingWifiList)); + } + + /// WiFi 连接状态: [0x81][success][reason] + void _handleWifiStatus(List data) { + if (data.length < 3) return; + final success = data[1] == 1; + final reason = data[2]; + debugPrint('[BLE Prov] WiFi 状态: success=$success, reason=$reason'); + _wifiStatusController.add(WifiResult( + success: success, + reasonCode: reason, + staMac: _lastStaMac, + )); + } + + String? _lastStaMac; + + /// 自定义数据: [0x84][payload...] 如 "STA_MAC:AA:BB:CC:DD:EE:FF" + void _handleCustomData(List data) { + if (data.length < 2) return; + final payload = String.fromCharCodes(data.sublist(1)); + debugPrint('[BLE Prov] 自定义数据: $payload'); + if (payload.startsWith('STA_MAC:')) { + _lastStaMac = payload.substring(8); + debugPrint('[BLE Prov] 设备 STA MAC: $_lastStaMac'); + } + } +} diff --git a/airhub_app/lib/core/services/log_center_service.dart b/airhub_app/lib/core/services/log_center_service.dart new file mode 100644 index 0000000..192cab2 --- /dev/null +++ b/airhub_app/lib/core/services/log_center_service.dart @@ -0,0 +1,142 @@ +import 'dart:isolate'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'log_center_service.g.dart'; + +@Riverpod(keepAlive: true) +LogCenterService logCenterService(Ref ref) { + return LogCenterService(); +} + +class LogCenterService { + static const String _url = + 'https://qiyuan-log-center-api.airlabs.art/api/v1/logs/report'; + static const String _projectId = 'airhub_app'; + + late final Dio _dio; + + LogCenterService() { + _dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 3), + headers: {'Content-Type': 'application/json'}, + )); + } + + /// 上报 Flutter 框架错误(FlutterError) + void reportFlutterError(FlutterErrorDetails details) { + _report( + errorType: 'FlutterError', + message: details.exceptionAsString(), + stackTrace: details.stack, + context: { + 'library': details.library ?? 'unknown', + 'context': details.context?.toString() ?? '', + }, + ); + } + + /// 上报未捕获的异步异常(Zone / PlatformDispatcher) + void reportUncaughtError(Object error, StackTrace? stack) { + _report( + errorType: error.runtimeType.toString(), + message: error.toString(), + stackTrace: stack, + ); + } + + /// 上报业务层手动捕获的异常 + void reportError( + Object error, { + StackTrace? stackTrace, + Map? context, + }) { + _report( + errorType: error.runtimeType.toString(), + message: error.toString(), + stackTrace: stackTrace, + context: context, + ); + } + + /// 核心上报逻辑 — 异步、静默、不阻塞 UI + void _report({ + required String errorType, + required String message, + StackTrace? stackTrace, + Map? context, + }) { + // 解析堆栈第一帧 + String filePath = 'unknown'; + int lineNumber = 0; + final frames = stackTrace?.toString().split('\n') ?? []; + if (frames.isNotEmpty) { + final match = + RegExp(r'(?:package:airhub_app/|lib/)(.+?):(\d+)').firstMatch( + frames.firstWhere( + (f) => f.contains('package:airhub_app/') || f.contains('lib/'), + orElse: () => frames.first, + ), + ); + if (match != null) { + filePath = 'lib/${match.group(1)}'; + lineNumber = int.tryParse(match.group(2) ?? '0') ?? 0; + } + } + + final payload = { + 'project_id': _projectId, + 'environment': kDebugMode ? 'development' : 'production', + 'level': 'ERROR', + 'error': { + 'type': errorType, + 'message': message.length > 2000 ? message.substring(0, 2000) : message, + 'file_path': filePath, + 'line_number': lineNumber, + 'stack_trace': frames.take(30).toList(), + }, + 'context': { + 'platform': defaultTargetPlatform.name, + 'is_web': kIsWeb, + ...?context, + }, + }; + + // 异步发送,不阻塞调用方 + _sendAsync(payload); + } + + void _sendAsync(Map payload) { + // Web 不支持 Isolate,用 compute / 直接 fire-and-forget + if (kIsWeb) { + _send(payload); + } else { + // 原生平台用 Isolate 完全不阻塞 UI 线程 + Isolate.run(() => _sendStatic(payload)); + } + } + + Future _send(Map payload) async { + try { + await _dio.post(_url, data: payload); + } catch (_) { + // 静默失败,不影响 App 运行 + } + } + + /// Isolate 内使用的静态方法(独立 Dio 实例) + static Future _sendStatic(Map payload) async { + try { + final dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 3), + headers: {'Content-Type': 'application/json'}, + )); + await dio.post(_url, data: payload); + } catch (_) { + // 静默失败 + } + } +} diff --git a/airhub_app/lib/core/services/log_center_service.g.dart b/airhub_app/lib/core/services/log_center_service.g.dart new file mode 100644 index 0000000..c74dd97 --- /dev/null +++ b/airhub_app/lib/core/services/log_center_service.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'log_center_service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(logCenterService) +const logCenterServiceProvider = LogCenterServiceProvider._(); + +final class LogCenterServiceProvider + extends + $FunctionalProvider< + LogCenterService, + LogCenterService, + LogCenterService + > + with $Provider { + const LogCenterServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'logCenterServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$logCenterServiceHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + LogCenterService create(Ref ref) { + return logCenterService(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(LogCenterService value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$logCenterServiceHash() => r'd32eef012bfcebde414b77bfc69fa9ffda09eb5e'; diff --git a/airhub_app/lib/core/services/phone_auth_service.dart b/airhub_app/lib/core/services/phone_auth_service.dart new file mode 100644 index 0000000..30f44a8 --- /dev/null +++ b/airhub_app/lib/core/services/phone_auth_service.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart' show debugPrint, kIsWeb; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +// 本地 Web 调试:始终使用 stub(ali_auth 不兼容当前 Dart 版本) +import 'phone_auth_service_stub.dart'; + +part 'phone_auth_service.g.dart'; + +class PhoneAuthConfig { + static const String androidSk = + 'eLg3aWBZ2JOO6eX6bmrcSGqNSE3/jWafpIh1JYL2PIW4WxkQdrUUVCKFmeErwr+ZcqotmFc+zSDYZpyO6xVNfPB+C8KdaJwO19Sn7kbFX52Gv2T7neNTGXla+lwHj4lqpL5P2zmFNeZlTeYN9YuggsPc2IeX+6T3F26r1ih7HcCfMPECqUyTY9a0HT0CCsfwva1gfRAr2MN87I3yRGsr2IpPOBvGqUoa8cD9+8EBBQzouCZ5YbrE3MP2dISTHmx+8ORYEP6NT3BmPnPR6UVQEc6nTmbMMjjLMKFaMsi+M4gg5pgnEwYhd0GYB6oV+v15'; + static const String iosSk = + 'kl3UL3GomT2sxglYyXY9LeuXAxxej24SotVP1UAZij4NI3T7E5W3NKFVv61E3bUwugtfRyucSDkws25tzZ9LoBGEg4H19MD31YmwxhYsKlS5fe/+jdigjDXsNWonTLEmFqxGJqtav2i0M9Q5L5YQcQpHYWc2IpL3WT2dTCU876ghQIm8UXF8TYhwHNGLvdjuUbp1naGjwbWxzov2Fy2b0DOkb5q1gc0DWsKQ4XAQhzwcivO88VbT/7tuFFdpGxmpwETBS0u7pkeGan2ZCKxAEA=='; +} + +@Riverpod(keepAlive: true) +PhoneAuthService phoneAuthService(Ref ref) { + return PhoneAuthService(); +} + +class PhoneAuthService { + bool _initialized = false; + String? _lastError; + + /// 最近一次错误信息(用于 UI 展示) + String? get lastError => _lastError; + + /// 初始化 SDK(只需调用一次) + Future init() async { + debugPrint('[AliAuth] init() called, _initialized=$_initialized, kIsWeb=$kIsWeb'); + if (_initialized) return; + if (kIsWeb) { + _lastError = '不支持 Web 平台'; + return; + } + + try { + await AliAuth.initSdk( + AliAuthModel( + PhoneAuthConfig.androidSk, + PhoneAuthConfig.iosSk, + isDebug: true, + autoQuitPage: true, + pageType: PageType.fullPort, + ), + ); + _initialized = true; + _lastError = null; + debugPrint('[AliAuth] SDK 初始化成功'); + } catch (e) { + _initialized = false; + _lastError = 'SDK初始化失败: $e'; + debugPrint('[AliAuth] $_lastError'); + } + } + + /// 一键登录,返回阿里云 token(用于发给后端换手机号) + /// 返回 null 表示用户取消或认证失败 + Future getLoginToken() async { + debugPrint('[AliAuth] getLoginToken() called, _initialized=$_initialized'); + if (!_initialized) { + await init(); + } + if (!_initialized) { + debugPrint('[AliAuth] SDK 未初始化,返回 null, error=$_lastError'); + return null; + } + + final completer = Completer(); + + AliAuth.loginListen(onEvent: (event) { + debugPrint('[AliAuth] loginListen event: $event'); + final code = event['code'] as String?; + + if (code == '600000' && event['data'] != null) { + if (!completer.isCompleted) { + completer.complete(event['data'] as String); + } + } else if (code == '700000' || code == '700001') { + _lastError = '用户取消'; + if (!completer.isCompleted) { + completer.complete(null); + } + } else if (code != null && code.startsWith('6') && code != '600000') { + _lastError = '错误码$code: ${event['msg']}'; + debugPrint('[AliAuth] $_lastError'); + if (!completer.isCompleted) { + completer.complete(null); + } + } + }); + + return completer.future.timeout( + const Duration(seconds: 30), + onTimeout: () { + _lastError = '请求超时(30s)'; + debugPrint('[AliAuth] $_lastError'); + return null; + }, + ); + } +} diff --git a/airhub_app/lib/core/services/phone_auth_service.g.dart b/airhub_app/lib/core/services/phone_auth_service.g.dart new file mode 100644 index 0000000..e9c6387 --- /dev/null +++ b/airhub_app/lib/core/services/phone_auth_service.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'phone_auth_service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(phoneAuthService) +const phoneAuthServiceProvider = PhoneAuthServiceProvider._(); + +final class PhoneAuthServiceProvider + extends + $FunctionalProvider< + PhoneAuthService, + PhoneAuthService, + PhoneAuthService + > + with $Provider { + const PhoneAuthServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'phoneAuthServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$phoneAuthServiceHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + PhoneAuthService create(Ref ref) { + return phoneAuthService(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(PhoneAuthService value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$phoneAuthServiceHash() => r'b7a4a2481eef4a3ddd0ce529c879dc5319a3106b'; diff --git a/airhub_app/lib/core/services/phone_auth_service_stub.dart b/airhub_app/lib/core/services/phone_auth_service_stub.dart new file mode 100644 index 0000000..802866f --- /dev/null +++ b/airhub_app/lib/core/services/phone_auth_service_stub.dart @@ -0,0 +1,14 @@ +/// Web 平台 stub — ali_auth 不支持 Web,提供空实现 +class AliAuth { + static Future initSdk(dynamic model) async {} + static void loginListen({required Function(Map) onEvent}) {} +} + +class AliAuthModel { + AliAuthModel(String androidSk, String iosSk, + {bool isDebug = false, bool autoQuitPage = false, dynamic pageType}); +} + +class PageType { + static const fullPort = 0; +} diff --git a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart index 5bb74a9..83c714c 100644 --- a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart +++ b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart @@ -1,39 +1,72 @@ -import 'dart:async'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; +import '../../domain/entities/auth_tokens.dart'; import '../../domain/entities/user.dart'; part 'auth_remote_data_source.g.dart'; abstract class AuthRemoteDataSource { - Future loginWithPhone(String phoneNumber, String code); - Future oneClickLogin(); + Future sendCode(String phone); + Future<({User user, AuthTokens tokens, bool isNewUser})> codeLogin( + String phone, String code); + Future<({User user, AuthTokens tokens, bool isNewUser})> tokenLogin( + String token); + Future logout(String refreshToken); + Future deleteAccount(); } @riverpod AuthRemoteDataSource authRemoteDataSource(Ref ref) { - return AuthRemoteDataSourceImpl(); + final apiClient = ref.watch(apiClientProvider); + return AuthRemoteDataSourceImpl(apiClient); } class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + final ApiClient _apiClient; + + AuthRemoteDataSourceImpl(this._apiClient); + @override - Future loginWithPhone(String phoneNumber, String code) async { - // Mock network delay and logic copied from original login_page.dart - await Future.delayed(const Duration(milliseconds: 1500)); - // Simulate successful login - return User( - id: '1', - phoneNumber: phoneNumber, - nickname: 'User ${phoneNumber.substring(7)}', - ); + Future sendCode(String phone) async { + await _apiClient.post('/auth/send-code/', data: {'phone': phone}); } @override - Future oneClickLogin() async { - await Future.delayed(const Duration(milliseconds: 1500)); - return const User( - id: '2', - phoneNumber: '13800138000', - nickname: 'OneClick User', + Future<({User user, AuthTokens tokens, bool isNewUser})> codeLogin( + String phone, String code) async { + final data = await _apiClient.post( + '/auth/code-login/', + data: {'phone': phone, 'code': code}, ); + return _parseLoginResponse(data as Map); + } + + @override + Future<({User user, AuthTokens tokens, bool isNewUser})> tokenLogin( + String token) async { + final data = await _apiClient.post( + '/auth/phone-login/', + data: {'token': token}, + ); + return _parseLoginResponse(data as Map); + } + + @override + Future logout(String refreshToken) async { + await _apiClient.post('/auth/logout/', data: {'refresh': refreshToken}); + } + + @override + Future deleteAccount() async { + await _apiClient.delete('/auth/account/'); + } + + ({User user, AuthTokens tokens, bool isNewUser}) _parseLoginResponse( + Map data) { + final user = User.fromJson(data['user'] as Map); + // 后端返回字段名为 'token'(非 'tokens') + final tokens = AuthTokens.fromJson(data['token'] as Map); + final isNewUser = data['is_new_user'] as bool? ?? false; + return (user: user, tokens: tokens, isNewUser: isNewUser); } } diff --git a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart index d0872c1..41e3fc8 100644 --- a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart +++ b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart @@ -55,4 +55,4 @@ final class AuthRemoteDataSourceProvider } String _$authRemoteDataSourceHash() => - r'b6a9edd1b6c48be8564688bac362316f598b4432'; + r'9f874814620b5a8bcdf56417a68ed7cba404a9e9'; diff --git a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart index ce2fc96..c2d0eaf 100644 --- a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart +++ b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -1,51 +1,121 @@ +import 'dart:async'; import 'package:fpdart/fpdart.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/errors/exceptions.dart'; import '../../../../core/errors/failures.dart'; +import '../../../../core/network/token_manager.dart'; import '../../domain/entities/user.dart'; import '../../domain/repositories/auth_repository.dart'; import '../datasources/auth_remote_data_source.dart'; part 'auth_repository_impl.g.dart'; -@riverpod +@Riverpod(keepAlive: true) AuthRepository authRepository(Ref ref) { final remoteDataSource = ref.watch(authRemoteDataSourceProvider); - return AuthRepositoryImpl(remoteDataSource); + final tokenManager = ref.watch(tokenManagerProvider); + return AuthRepositoryImpl(remoteDataSource, tokenManager); } class AuthRepositoryImpl implements AuthRepository { final AuthRemoteDataSource _remoteDataSource; + final TokenManager _tokenManager; + final _authStateController = StreamController.broadcast(); + User? _currentUser; - AuthRepositoryImpl(this._remoteDataSource); + AuthRepositoryImpl(this._remoteDataSource, this._tokenManager); @override - Stream get authStateChanges => Stream.value(null); // Mock stream + User? get currentUser => _currentUser; @override - Future> loginWithPhone( - String phoneNumber, - String code, - ) async { + Stream get authStateChanges => _authStateController.stream; + + @override + Future> sendCode(String phone) async { try { - final user = await _remoteDataSource.loginWithPhone(phoneNumber, code); - return right(user); - } catch (e) { - return left(const ServerFailure('Login failed')); + await _remoteDataSource.sendCode(phone); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); } } @override - Future> oneClickLogin() async { + Future> codeLogin(String phone, String code) async { try { - final user = await _remoteDataSource.oneClickLogin(); - return right(user); - } catch (e) { - return left(const ServerFailure('One-click login failed')); + final result = await _remoteDataSource.codeLogin(phone, code); + await _tokenManager.saveTokens( + access: result.tokens.access, + refresh: result.tokens.refresh, + ); + _currentUser = result.user; + _authStateController.add(result.user); + return right(result.user); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> tokenLogin(String token) async { + try { + final result = await _remoteDataSource.tokenLogin(token); + await _tokenManager.saveTokens( + access: result.tokens.access, + refresh: result.tokens.refresh, + ); + _currentUser = result.user; + _authStateController.add(result.user); + return right(result.user); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); } } @override Future> logout() async { - return right(null); + try { + final refreshToken = await _tokenManager.getRefreshToken(); + if (refreshToken != null) { + await _remoteDataSource.logout(refreshToken); + } + await _tokenManager.clearTokens(); + _currentUser = null; + _authStateController.add(null); + return right(null); + } on ServerException catch (_) { + // 即使 API 失败也清除本地 token + await _tokenManager.clearTokens(); + _currentUser = null; + _authStateController.add(null); + return right(null); + } on NetworkException catch (_) { + await _tokenManager.clearTokens(); + _currentUser = null; + _authStateController.add(null); + return right(null); + } + } + + @override + Future> deleteAccount() async { + try { + await _remoteDataSource.deleteAccount(); + await _tokenManager.clearTokens(); + _currentUser = null; + _authStateController.add(null); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } } } diff --git a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart index dba5767..9cebe11 100644 --- a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart +++ b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart @@ -21,7 +21,7 @@ final class AuthRepositoryProvider argument: null, retry: null, name: r'authRepositoryProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -48,4 +48,4 @@ final class AuthRepositoryProvider } } -String _$authRepositoryHash() => r'43e05b07a705006cf920b080f78421ecc8bab1d9'; +String _$authRepositoryHash() => r'4f799a30b753954fa92b1ed1b21277bd6020a4a0'; diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart new file mode 100644 index 0000000..cd43231 --- /dev/null +++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_tokens.freezed.dart'; +part 'auth_tokens.g.dart'; + +@freezed +abstract class AuthTokens with _$AuthTokens { + const factory AuthTokens({ + required String access, + required String refresh, + }) = _AuthTokens; + + factory AuthTokens.fromJson(Map json) => + _$AuthTokensFromJson(json); +} diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart new file mode 100644 index 0000000..9dbd7b7 --- /dev/null +++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart @@ -0,0 +1,280 @@ +// 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 'auth_tokens.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$AuthTokens { + + String get access; String get refresh; +/// Create a copy of AuthTokens +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AuthTokensCopyWith get copyWith => _$AuthTokensCopyWithImpl(this as AuthTokens, _$identity); + + /// Serializes this AuthTokens to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthTokens&&(identical(other.access, access) || other.access == access)&&(identical(other.refresh, refresh) || other.refresh == refresh)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,access,refresh); + +@override +String toString() { + return 'AuthTokens(access: $access, refresh: $refresh)'; +} + + +} + +/// @nodoc +abstract mixin class $AuthTokensCopyWith<$Res> { + factory $AuthTokensCopyWith(AuthTokens value, $Res Function(AuthTokens) _then) = _$AuthTokensCopyWithImpl; +@useResult +$Res call({ + String access, String refresh +}); + + + + +} +/// @nodoc +class _$AuthTokensCopyWithImpl<$Res> + implements $AuthTokensCopyWith<$Res> { + _$AuthTokensCopyWithImpl(this._self, this._then); + + final AuthTokens _self; + final $Res Function(AuthTokens) _then; + +/// Create a copy of AuthTokens +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? access = null,Object? refresh = null,}) { + return _then(_self.copyWith( +access: null == access ? _self.access : access // ignore: cast_nullable_to_non_nullable +as String,refresh: null == refresh ? _self.refresh : refresh // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AuthTokens]. +extension AuthTokensPatterns on AuthTokens { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AuthTokens value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AuthTokens() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AuthTokens value) $default,){ +final _that = this; +switch (_that) { +case _AuthTokens(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AuthTokens value)? $default,){ +final _that = this; +switch (_that) { +case _AuthTokens() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String access, String refresh)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AuthTokens() when $default != null: +return $default(_that.access,_that.refresh);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String access, String refresh) $default,) {final _that = this; +switch (_that) { +case _AuthTokens(): +return $default(_that.access,_that.refresh);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String access, String refresh)? $default,) {final _that = this; +switch (_that) { +case _AuthTokens() when $default != null: +return $default(_that.access,_that.refresh);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _AuthTokens implements AuthTokens { + const _AuthTokens({required this.access, required this.refresh}); + factory _AuthTokens.fromJson(Map json) => _$AuthTokensFromJson(json); + +@override final String access; +@override final String refresh; + +/// Create a copy of AuthTokens +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AuthTokensCopyWith<_AuthTokens> get copyWith => __$AuthTokensCopyWithImpl<_AuthTokens>(this, _$identity); + +@override +Map toJson() { + return _$AuthTokensToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AuthTokens&&(identical(other.access, access) || other.access == access)&&(identical(other.refresh, refresh) || other.refresh == refresh)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,access,refresh); + +@override +String toString() { + return 'AuthTokens(access: $access, refresh: $refresh)'; +} + + +} + +/// @nodoc +abstract mixin class _$AuthTokensCopyWith<$Res> implements $AuthTokensCopyWith<$Res> { + factory _$AuthTokensCopyWith(_AuthTokens value, $Res Function(_AuthTokens) _then) = __$AuthTokensCopyWithImpl; +@override @useResult +$Res call({ + String access, String refresh +}); + + + + +} +/// @nodoc +class __$AuthTokensCopyWithImpl<$Res> + implements _$AuthTokensCopyWith<$Res> { + __$AuthTokensCopyWithImpl(this._self, this._then); + + final _AuthTokens _self; + final $Res Function(_AuthTokens) _then; + +/// Create a copy of AuthTokens +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? access = null,Object? refresh = null,}) { + return _then(_AuthTokens( +access: null == access ? _self.access : access // ignore: cast_nullable_to_non_nullable +as String,refresh: null == refresh ? _self.refresh : refresh // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart new file mode 100644 index 0000000..e051bc7 --- /dev/null +++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_tokens.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_AuthTokens _$AuthTokensFromJson(Map json) => _AuthTokens( + access: json['access'] as String, + refresh: json['refresh'] as String, +); + +Map _$AuthTokensToJson(_AuthTokens instance) => + {'access': instance.access, 'refresh': instance.refresh}; diff --git a/airhub_app/lib/features/auth/domain/entities/user.dart b/airhub_app/lib/features/auth/domain/entities/user.dart index 663a2e5..5e069e7 100644 --- a/airhub_app/lib/features/auth/domain/entities/user.dart +++ b/airhub_app/lib/features/auth/domain/entities/user.dart @@ -6,10 +6,12 @@ part 'user.g.dart'; @freezed abstract class User with _$User { const factory User({ - required String id, - required String phoneNumber, + required int id, + required String phone, String? nickname, - String? avatarUrl, + String? avatar, + String? gender, + String? birthday, }) = _User; factory User.fromJson(Map json) => _$UserFromJson(json); diff --git a/airhub_app/lib/features/auth/domain/entities/user.freezed.dart b/airhub_app/lib/features/auth/domain/entities/user.freezed.dart index 9ba14f0..2a0c9cb 100644 --- a/airhub_app/lib/features/auth/domain/entities/user.freezed.dart +++ b/airhub_app/lib/features/auth/domain/entities/user.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$User { - String get id; String get phoneNumber; String? get nickname; String? get avatarUrl; + int get id; String get phone; String? get nickname; String? get avatar; String? get gender; String? get birthday; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $UserCopyWith get copyWith => _$UserCopyWithImpl(this as User, _$ide @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is User&&(identical(other.id, id) || other.id == id)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatarUrl, avatarUrl) || other.avatarUrl == avatarUrl)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is User&&(identical(other.id, id) || other.id == id)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.birthday, birthday) || other.birthday == birthday)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,phoneNumber,nickname,avatarUrl); +int get hashCode => Object.hash(runtimeType,id,phone,nickname,avatar,gender,birthday); @override String toString() { - return 'User(id: $id, phoneNumber: $phoneNumber, nickname: $nickname, avatarUrl: $avatarUrl)'; + return 'User(id: $id, phone: $phone, nickname: $nickname, avatar: $avatar, gender: $gender, birthday: $birthday)'; } @@ -48,7 +48,7 @@ abstract mixin class $UserCopyWith<$Res> { factory $UserCopyWith(User value, $Res Function(User) _then) = _$UserCopyWithImpl; @useResult $Res call({ - String id, String phoneNumber, String? nickname, String? avatarUrl + int id, String phone, String? nickname, String? avatar, String? gender, String? birthday }); @@ -65,12 +65,14 @@ class _$UserCopyWithImpl<$Res> /// Create a copy of User /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? phoneNumber = null,Object? nickname = freezed,Object? avatarUrl = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? phone = null,Object? nickname = freezed,Object? avatar = freezed,Object? gender = freezed,Object? birthday = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable +as int,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable as String,nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable -as String?,avatarUrl: freezed == avatarUrl ? _self.avatarUrl : avatarUrl // ignore: cast_nullable_to_non_nullable +as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as String?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable +as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable as String?, )); } @@ -156,10 +158,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _User() when $default != null: -return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _: +return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _: return orElse(); } @@ -177,10 +179,10 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday) $default,) {final _that = this; switch (_that) { case _User(): -return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _: +return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _: throw StateError('Unexpected subclass'); } @@ -197,10 +199,10 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday)? $default,) {final _that = this; switch (_that) { case _User() when $default != null: -return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _: +return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _: return null; } @@ -212,13 +214,15 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case @JsonSerializable() class _User implements User { - const _User({required this.id, required this.phoneNumber, this.nickname, this.avatarUrl}); + const _User({required this.id, required this.phone, this.nickname, this.avatar, this.gender, this.birthday}); factory _User.fromJson(Map json) => _$UserFromJson(json); -@override final String id; -@override final String phoneNumber; +@override final int id; +@override final String phone; @override final String? nickname; -@override final String? avatarUrl; +@override final String? avatar; +@override final String? gender; +@override final String? birthday; /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @@ -233,16 +237,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _User&&(identical(other.id, id) || other.id == id)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatarUrl, avatarUrl) || other.avatarUrl == avatarUrl)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _User&&(identical(other.id, id) || other.id == id)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.birthday, birthday) || other.birthday == birthday)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,phoneNumber,nickname,avatarUrl); +int get hashCode => Object.hash(runtimeType,id,phone,nickname,avatar,gender,birthday); @override String toString() { - return 'User(id: $id, phoneNumber: $phoneNumber, nickname: $nickname, avatarUrl: $avatarUrl)'; + return 'User(id: $id, phone: $phone, nickname: $nickname, avatar: $avatar, gender: $gender, birthday: $birthday)'; } @@ -253,7 +257,7 @@ abstract mixin class _$UserCopyWith<$Res> implements $UserCopyWith<$Res> { factory _$UserCopyWith(_User value, $Res Function(_User) _then) = __$UserCopyWithImpl; @override @useResult $Res call({ - String id, String phoneNumber, String? nickname, String? avatarUrl + int id, String phone, String? nickname, String? avatar, String? gender, String? birthday }); @@ -270,12 +274,14 @@ class __$UserCopyWithImpl<$Res> /// Create a copy of User /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? phoneNumber = null,Object? nickname = freezed,Object? avatarUrl = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? phone = null,Object? nickname = freezed,Object? avatar = freezed,Object? gender = freezed,Object? birthday = freezed,}) { return _then(_User( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable +as int,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable as String,nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable -as String?,avatarUrl: freezed == avatarUrl ? _self.avatarUrl : avatarUrl // ignore: cast_nullable_to_non_nullable +as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as String?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable +as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable as String?, )); } diff --git a/airhub_app/lib/features/auth/domain/entities/user.g.dart b/airhub_app/lib/features/auth/domain/entities/user.g.dart index 5306719..9de667e 100644 --- a/airhub_app/lib/features/auth/domain/entities/user.g.dart +++ b/airhub_app/lib/features/auth/domain/entities/user.g.dart @@ -7,15 +7,19 @@ part of 'user.dart'; // ************************************************************************** _User _$UserFromJson(Map json) => _User( - id: json['id'] as String, - phoneNumber: json['phoneNumber'] as String, + id: (json['id'] as num).toInt(), + phone: json['phone'] as String, nickname: json['nickname'] as String?, - avatarUrl: json['avatarUrl'] as String?, + avatar: json['avatar'] as String?, + gender: json['gender'] as String?, + birthday: json['birthday'] as String?, ); Map _$UserToJson(_User instance) => { 'id': instance.id, - 'phoneNumber': instance.phoneNumber, + 'phone': instance.phone, 'nickname': instance.nickname, - 'avatarUrl': instance.avatarUrl, + 'avatar': instance.avatar, + 'gender': instance.gender, + 'birthday': instance.birthday, }; diff --git a/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart b/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart index e647e75..b298bba 100644 --- a/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart +++ b/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart @@ -3,8 +3,11 @@ import '../../../../core/errors/failures.dart'; import '../entities/user.dart'; abstract class AuthRepository { - Future> loginWithPhone(String phoneNumber, String code); - Future> oneClickLogin(); + Future> sendCode(String phone); + Future> codeLogin(String phone, String code); + Future> tokenLogin(String token); Future> logout(); + Future> deleteAccount(); Stream get authStateChanges; + User? get currentUser; } diff --git a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart index d91392c..b3a1b6e 100644 --- a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart +++ b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart @@ -10,23 +10,76 @@ class AuthController extends _$AuthController { // Initial state is void (idle) } - Future loginWithPhone(String phoneNumber, String code) async { + Future sendCode(String phone) async { state = const AsyncLoading(); final repository = ref.read(authRepositoryProvider); - final result = await repository.loginWithPhone(phoneNumber, code); + final result = await repository.sendCode(phone); + if (!ref.mounted) return; state = result.fold( (failure) => AsyncError(failure.message, StackTrace.current), - (user) => const AsyncData(null), + (_) => const AsyncData(null), ); } - Future oneClickLogin() async { + Future codeLogin(String phone, String code) async { state = const AsyncLoading(); final repository = ref.read(authRepositoryProvider); - final result = await repository.oneClickLogin(); + final result = await repository.codeLogin(phone, code); + if (!ref.mounted) return false; + return result.fold( + (failure) { + state = AsyncError(failure.message, StackTrace.current); + return false; + }, + (user) { + state = const AsyncData(null); + return true; + }, + ); + } + + Future tokenLogin(String token) async { + state = const AsyncLoading(); + final repository = ref.read(authRepositoryProvider); + final result = await repository.tokenLogin(token); + if (!ref.mounted) return false; + return result.fold( + (failure) { + state = AsyncError(failure.message, StackTrace.current); + return false; + }, + (user) { + state = const AsyncData(null); + return true; + }, + ); + } + + Future logout() async { + state = const AsyncLoading(); + final repository = ref.read(authRepositoryProvider); + final result = await repository.logout(); + if (!ref.mounted) return; state = result.fold( (failure) => AsyncError(failure.message, StackTrace.current), - (user) => const AsyncData(null), + (_) => const AsyncData(null), + ); + } + + Future deleteAccount() async { + state = const AsyncLoading(); + final repository = ref.read(authRepositoryProvider); + final result = await repository.deleteAccount(); + if (!ref.mounted) return false; + return result.fold( + (failure) { + state = AsyncError(failure.message, StackTrace.current); + return false; + }, + (_) { + state = const AsyncData(null); + return true; + }, ); } } diff --git a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart index c3d053b..82a5a8a 100644 --- a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart +++ b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart @@ -33,7 +33,7 @@ final class AuthControllerProvider AuthController create() => AuthController(); } -String _$authControllerHash() => r'e7278df3deb6222da1e5e8b8b6ec921493441758'; +String _$authControllerHash() => r'3a290ddd5b4b091786d5020ecb57b7fb1d3a287a'; abstract class _$AuthController extends $AsyncNotifier { FutureOr build(); diff --git a/airhub_app/lib/features/auth/presentation/pages/login_page.dart b/airhub_app/lib/features/auth/presentation/pages/login_page.dart index 4548cba..bf9e330 100644 --- a/airhub_app/lib/features/auth/presentation/pages/login_page.dart +++ b/airhub_app/lib/features/auth/presentation/pages/login_page.dart @@ -7,10 +7,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/services/phone_auth_service.dart'; import '../../../../theme/app_colors.dart'; import '../../../../widgets/animated_gradient_background.dart'; import '../../../../widgets/gradient_button.dart'; import '../../../../widgets/ios_toast.dart'; +import '../../../device/presentation/controllers/device_controller.dart'; import '../controllers/auth_controller.dart'; import '../widgets/floating_mascot.dart'; @@ -49,17 +51,11 @@ class _LoginPageState extends ConsumerState { super.dispose(); } - void _handleListener(BuildContext context, AsyncValue next) { + void _handleListener(BuildContext context, AsyncValue? prev, AsyncValue next) { next.whenOrNull( error: (error, stack) { _showToast(error.toString(), isError: true); }, - data: (_) { - // Navigate to Home on success - if (mounted) { - context.go('/home'); - } - }, ); } @@ -209,11 +205,26 @@ class _LoginPageState extends ConsumerState { } // Logic Methods - void _doOneClickLogin() { - ref.read(authControllerProvider.notifier).oneClickLogin(); + Future _doOneClickLogin() async { + debugPrint('[Login] _doOneClickLogin() 开始'); + final phoneAuthService = ref.read(phoneAuthServiceProvider); + final token = await phoneAuthService.getLoginToken(); + debugPrint('[Login] getLoginToken 返回: $token'); + if (token == null) { + final error = phoneAuthService.lastError ?? '未知错误'; + if (mounted) _showToast('一键登录失败: $error', isError: true); + return; + } + if (!mounted) return; + final success = await ref.read(authControllerProvider.notifier).tokenLogin(token); + debugPrint('[Login] tokenLogin 结果: $success'); + if (success && mounted) { + await _navigateAfterLogin(); + } } void _handleOneClickLogin() { + debugPrint('[Login] _handleOneClickLogin() agreed=$_agreed'); if (!_agreed) { _showAgreementDialog(action: 'oneclick'); return; @@ -234,28 +245,55 @@ class _LoginPageState extends ConsumerState { AppToast.show(context, message, isError: isError); } - void _sendCode() { + Future _sendCode() async { if (!_isValidPhone(_phoneController.text)) { _showToast('请输入正确的手机号', isError: true); return; } - setState(() => _countdown = 60); - _showToast('验证码已发送'); - _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_countdown <= 1) { - timer.cancel(); - if (mounted) setState(() => _countdown = 0); - } else { - if (mounted) setState(() => _countdown--); - } - }); + try { + await ref.read(authControllerProvider.notifier).sendCode(_phoneController.text); + if (!mounted) return; + setState(() => _countdown = 60); + _showToast('验证码已发送'); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_countdown <= 1) { + timer.cancel(); + if (mounted) setState(() => _countdown = 0); + } else { + if (mounted) setState(() => _countdown--); + } + }); + } catch (_) { + // 错误已通过 listener 处理 + } } - void _submitSmsLogin() { + Future _submitSmsLogin() async { if (!_canSubmitSms) return; - ref + final success = await ref .read(authControllerProvider.notifier) - .loginWithPhone(_phoneController.text, _codeController.text); + .codeLogin(_phoneController.text, _codeController.text); + if (success && mounted) { + await _navigateAfterLogin(); + } + } + + Future _navigateAfterLogin() async { + if (!mounted) return; + try { + final devices = await ref.read(deviceControllerProvider.future); + if (!mounted) return; + if (devices.isNotEmpty) { + debugPrint('[Login] User has ${devices.length} device(s), navigating to device control'); + context.go('/device-control'); + } else { + debugPrint('[Login] No devices, navigating to home'); + context.go('/home'); + } + } catch (e) { + debugPrint('[Login] Device check failed: $e'); + if (mounted) context.go('/home'); + } } Widget _buildGradientBackground() { @@ -267,7 +305,7 @@ class _LoginPageState extends ConsumerState { // Listen to Auth State ref.listen( authControllerProvider, - (_, next) => _handleListener(context, next), + (prev, next) => _handleListener(context, prev, next), ); final isLoading = ref.watch(authControllerProvider).isLoading; diff --git a/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart new file mode 100644 index 0000000..95bed06 --- /dev/null +++ b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart @@ -0,0 +1,115 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; +import '../../domain/entities/device.dart'; +import '../../domain/entities/device_detail.dart'; + +part 'device_remote_data_source.g.dart'; + +abstract class DeviceRemoteDataSource { + /// GET /devices/query-by-mac/?mac=xxx (无需认证) + Future> queryByMac(String mac); + + /// POST /devices/verify/ + Future> verifyDevice(String sn); + + /// POST /devices/bind/ + Future bindDevice(String sn, {int? spiritId}); + + /// GET /devices/my_devices/ + Future> getMyDevices(); + + /// GET /devices/{id}/detail/ + Future getDeviceDetail(int userDeviceId); + + /// DELETE /devices/{id}/unbind/ + Future unbindDevice(int userDeviceId); + + /// PUT /devices/{id}/update-spirit/ + Future updateSpirit(int userDeviceId, int spiritId); + + /// PUT /devices/{id}/settings/ + Future updateSettings(int userDeviceId, Map settings); + + /// POST /devices/{id}/wifi/ + Future configWifi(int userDeviceId, String ssid); +} + +@riverpod +DeviceRemoteDataSource deviceRemoteDataSource(Ref ref) { + final apiClient = ref.watch(apiClientProvider); + return DeviceRemoteDataSourceImpl(apiClient); +} + +class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource { + final ApiClient _apiClient; + + DeviceRemoteDataSourceImpl(this._apiClient); + + @override + Future> queryByMac(String mac) async { + final data = await _apiClient.get( + '/devices/query-by-mac/', + queryParameters: {'mac': mac}, + ); + return data as Map; + } + + @override + Future> verifyDevice(String sn) async { + final data = await _apiClient.post('/devices/verify/', data: {'sn': sn}); + return data as Map; + } + + @override + Future bindDevice(String sn, {int? spiritId}) async { + final body = {'sn': sn}; + if (spiritId != null) body['spirit_id'] = spiritId; + final data = await _apiClient.post('/devices/bind/', data: body); + return UserDevice.fromJson(data as Map); + } + + @override + Future> getMyDevices() async { + final data = await _apiClient.get('/devices/my_devices/'); + final list = data as List; + return list + .map((e) => UserDevice.fromJson(e as Map)) + .toList(); + } + + @override + Future getDeviceDetail(int userDeviceId) async { + final data = await _apiClient.get('/devices/$userDeviceId/detail/'); + return DeviceDetail.fromJson(data as Map); + } + + @override + Future unbindDevice(int userDeviceId) async { + await _apiClient.delete('/devices/$userDeviceId/unbind/'); + } + + @override + Future updateSpirit(int userDeviceId, int spiritId) async { + final data = await _apiClient.put( + '/devices/$userDeviceId/update-spirit/', + data: {'spirit_id': spiritId}, + ); + return UserDevice.fromJson(data as Map); + } + + @override + Future updateSettings( + int userDeviceId, + Map settings, + ) async { + await _apiClient.put('/devices/$userDeviceId/settings/', data: settings); + } + + @override + Future configWifi(int userDeviceId, String ssid) async { + await _apiClient.post( + '/devices/$userDeviceId/wifi/', + data: {'ssid': ssid}, + ); + } +} diff --git a/airhub_app/lib/features/device/data/datasources/device_remote_data_source.g.dart b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.g.dart new file mode 100644 index 0000000..020ad00 --- /dev/null +++ b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_remote_data_source.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(deviceRemoteDataSource) +const deviceRemoteDataSourceProvider = DeviceRemoteDataSourceProvider._(); + +final class DeviceRemoteDataSourceProvider + extends + $FunctionalProvider< + DeviceRemoteDataSource, + DeviceRemoteDataSource, + DeviceRemoteDataSource + > + with $Provider { + const DeviceRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + DeviceRemoteDataSource create(Ref ref) { + return deviceRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(DeviceRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$deviceRemoteDataSourceHash() => + r'cc457ef1f933b66a63014b7ebb123478077096c3'; diff --git a/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart b/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart new file mode 100644 index 0000000..368d52c --- /dev/null +++ b/airhub_app/lib/features/device/data/repositories/device_repository_impl.dart @@ -0,0 +1,147 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../../../../core/errors/failures.dart'; +import '../../domain/entities/device.dart'; +import '../../domain/entities/device_detail.dart'; +import '../../domain/repositories/device_repository.dart'; +import '../datasources/device_remote_data_source.dart'; + +part 'device_repository_impl.g.dart'; + +@riverpod +DeviceRepository deviceRepository(Ref ref) { + final remoteDataSource = ref.watch(deviceRemoteDataSourceProvider); + return DeviceRepositoryImpl(remoteDataSource); +} + +class DeviceRepositoryImpl implements DeviceRepository { + final DeviceRemoteDataSource _remoteDataSource; + + DeviceRepositoryImpl(this._remoteDataSource); + + @override + Future>> queryByMac(String mac) async { + try { + final result = await _remoteDataSource.queryByMac(mac); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future>> verifyDevice(String sn) async { + try { + final result = await _remoteDataSource.verifyDevice(sn); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> bindDevice( + String sn, { + int? spiritId, + }) async { + try { + final result = await _remoteDataSource.bindDevice(sn, spiritId: spiritId); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future>> getMyDevices() async { + try { + final result = await _remoteDataSource.getMyDevices(); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> getDeviceDetail( + int userDeviceId, + ) async { + try { + final result = await _remoteDataSource.getDeviceDetail(userDeviceId); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> unbindDevice(int userDeviceId) async { + try { + await _remoteDataSource.unbindDevice(userDeviceId); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> updateSpirit( + int userDeviceId, + int spiritId, + ) async { + try { + final result = await _remoteDataSource.updateSpirit( + userDeviceId, + spiritId, + ); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> updateSettings( + int userDeviceId, + Map settings, + ) async { + try { + await _remoteDataSource.updateSettings(userDeviceId, settings); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> configWifi( + int userDeviceId, + String ssid, + ) async { + try { + await _remoteDataSource.configWifi(userDeviceId, ssid); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } +} diff --git a/airhub_app/lib/features/device/data/repositories/device_repository_impl.g.dart b/airhub_app/lib/features/device/data/repositories/device_repository_impl.g.dart new file mode 100644 index 0000000..22e8187 --- /dev/null +++ b/airhub_app/lib/features/device/data/repositories/device_repository_impl.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_repository_impl.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(deviceRepository) +const deviceRepositoryProvider = DeviceRepositoryProvider._(); + +final class DeviceRepositoryProvider + extends + $FunctionalProvider< + DeviceRepository, + DeviceRepository, + DeviceRepository + > + with $Provider { + const DeviceRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + DeviceRepository create(Ref ref) { + return deviceRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(DeviceRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$deviceRepositoryHash() => r'54eaa070bb4dfb0e34704d4525db2472f239450c'; diff --git a/airhub_app/lib/features/device/domain/entities/device.dart b/airhub_app/lib/features/device/domain/entities/device.dart new file mode 100644 index 0000000..6ef7e40 --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device.dart @@ -0,0 +1,55 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'device.freezed.dart'; +part 'device.g.dart'; + +@freezed +abstract class DeviceType with _$DeviceType { + const factory DeviceType({ + required int id, + required String brand, + required String productCode, + required String name, + @Default(true) bool isNetworkRequired, + @Default(true) bool isActive, + String? createdAt, + }) = _DeviceType; + + factory DeviceType.fromJson(Map json) => + _$DeviceTypeFromJson(json); +} + +@freezed +abstract class DeviceInfo with _$DeviceInfo { + const factory DeviceInfo({ + required int id, + required String sn, + DeviceType? deviceType, + DeviceType? deviceTypeInfo, + String? macAddress, + @Default('') String name, + @Default('in_stock') String status, + @Default('') String firmwareVersion, + String? lastOnlineAt, + String? createdAt, + }) = _DeviceInfo; + + factory DeviceInfo.fromJson(Map json) => + _$DeviceInfoFromJson(json); +} + +@freezed +abstract class UserDevice with _$UserDevice { + const factory UserDevice({ + required int id, + required DeviceInfo device, + int? spirit, + String? spiritName, + @Default('owner') String bindType, + String? bindTime, + @Default(true) bool isActive, + }) = _UserDevice; + + factory UserDevice.fromJson(Map json) => + _$UserDeviceFromJson(json); +} diff --git a/airhub_app/lib/features/device/domain/entities/device.freezed.dart b/airhub_app/lib/features/device/domain/entities/device.freezed.dart new file mode 100644 index 0000000..a40b797 --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device.freezed.dart @@ -0,0 +1,932 @@ +// 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 'device.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$DeviceType { + + int get id; String get brand; String get productCode; String get name; bool get isNetworkRequired; bool get isActive; String? get createdAt; +/// Create a copy of DeviceType +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeviceTypeCopyWith get copyWith => _$DeviceTypeCopyWithImpl(this as DeviceType, _$identity); + + /// Serializes this DeviceType to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceType&&(identical(other.id, id) || other.id == id)&&(identical(other.brand, brand) || other.brand == brand)&&(identical(other.productCode, productCode) || other.productCode == productCode)&&(identical(other.name, name) || other.name == name)&&(identical(other.isNetworkRequired, isNetworkRequired) || other.isNetworkRequired == isNetworkRequired)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,brand,productCode,name,isNetworkRequired,isActive,createdAt); + +@override +String toString() { + return 'DeviceType(id: $id, brand: $brand, productCode: $productCode, name: $name, isNetworkRequired: $isNetworkRequired, isActive: $isActive, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +abstract mixin class $DeviceTypeCopyWith<$Res> { + factory $DeviceTypeCopyWith(DeviceType value, $Res Function(DeviceType) _then) = _$DeviceTypeCopyWithImpl; +@useResult +$Res call({ + int id, String brand, String productCode, String name, bool isNetworkRequired, bool isActive, String? createdAt +}); + + + + +} +/// @nodoc +class _$DeviceTypeCopyWithImpl<$Res> + implements $DeviceTypeCopyWith<$Res> { + _$DeviceTypeCopyWithImpl(this._self, this._then); + + final DeviceType _self; + final $Res Function(DeviceType) _then; + +/// Create a copy of DeviceType +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? brand = null,Object? productCode = null,Object? name = null,Object? isNetworkRequired = null,Object? isActive = null,Object? createdAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,brand: null == brand ? _self.brand : brand // ignore: cast_nullable_to_non_nullable +as String,productCode: null == productCode ? _self.productCode : productCode // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isNetworkRequired: null == isNetworkRequired ? _self.isNetworkRequired : isNetworkRequired // ignore: cast_nullable_to_non_nullable +as bool,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [DeviceType]. +extension DeviceTypePatterns on DeviceType { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DeviceType value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DeviceType() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DeviceType value) $default,){ +final _that = this; +switch (_that) { +case _DeviceType(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DeviceType value)? $default,){ +final _that = this; +switch (_that) { +case _DeviceType() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String brand, String productCode, String name, bool isNetworkRequired, bool isActive, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DeviceType() when $default != null: +return $default(_that.id,_that.brand,_that.productCode,_that.name,_that.isNetworkRequired,_that.isActive,_that.createdAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, String brand, String productCode, String name, bool isNetworkRequired, bool isActive, String? createdAt) $default,) {final _that = this; +switch (_that) { +case _DeviceType(): +return $default(_that.id,_that.brand,_that.productCode,_that.name,_that.isNetworkRequired,_that.isActive,_that.createdAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String brand, String productCode, String name, bool isNetworkRequired, bool isActive, String? createdAt)? $default,) {final _that = this; +switch (_that) { +case _DeviceType() when $default != null: +return $default(_that.id,_that.brand,_that.productCode,_that.name,_that.isNetworkRequired,_that.isActive,_that.createdAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _DeviceType implements DeviceType { + const _DeviceType({required this.id, required this.brand, required this.productCode, required this.name, this.isNetworkRequired = true, this.isActive = true, this.createdAt}); + factory _DeviceType.fromJson(Map json) => _$DeviceTypeFromJson(json); + +@override final int id; +@override final String brand; +@override final String productCode; +@override final String name; +@override@JsonKey() final bool isNetworkRequired; +@override@JsonKey() final bool isActive; +@override final String? createdAt; + +/// Create a copy of DeviceType +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DeviceTypeCopyWith<_DeviceType> get copyWith => __$DeviceTypeCopyWithImpl<_DeviceType>(this, _$identity); + +@override +Map toJson() { + return _$DeviceTypeToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceType&&(identical(other.id, id) || other.id == id)&&(identical(other.brand, brand) || other.brand == brand)&&(identical(other.productCode, productCode) || other.productCode == productCode)&&(identical(other.name, name) || other.name == name)&&(identical(other.isNetworkRequired, isNetworkRequired) || other.isNetworkRequired == isNetworkRequired)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,brand,productCode,name,isNetworkRequired,isActive,createdAt); + +@override +String toString() { + return 'DeviceType(id: $id, brand: $brand, productCode: $productCode, name: $name, isNetworkRequired: $isNetworkRequired, isActive: $isActive, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$DeviceTypeCopyWith<$Res> implements $DeviceTypeCopyWith<$Res> { + factory _$DeviceTypeCopyWith(_DeviceType value, $Res Function(_DeviceType) _then) = __$DeviceTypeCopyWithImpl; +@override @useResult +$Res call({ + int id, String brand, String productCode, String name, bool isNetworkRequired, bool isActive, String? createdAt +}); + + + + +} +/// @nodoc +class __$DeviceTypeCopyWithImpl<$Res> + implements _$DeviceTypeCopyWith<$Res> { + __$DeviceTypeCopyWithImpl(this._self, this._then); + + final _DeviceType _self; + final $Res Function(_DeviceType) _then; + +/// Create a copy of DeviceType +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? brand = null,Object? productCode = null,Object? name = null,Object? isNetworkRequired = null,Object? isActive = null,Object? createdAt = freezed,}) { + return _then(_DeviceType( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,brand: null == brand ? _self.brand : brand // ignore: cast_nullable_to_non_nullable +as String,productCode: null == productCode ? _self.productCode : productCode // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isNetworkRequired: null == isNetworkRequired ? _self.isNetworkRequired : isNetworkRequired // ignore: cast_nullable_to_non_nullable +as bool,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +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; 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) +@pragma('vm:prefer-inline') +$DeviceInfoCopyWith get copyWith => _$DeviceInfoCopyWithImpl(this as DeviceInfo, _$identity); + + /// Serializes this DeviceInfo to a JSON map. + Map 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.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,firmwareVersion,lastOnlineAt,createdAt); + +@override +String toString() { + return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +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, String firmwareVersion, String? lastOnlineAt, String? createdAt +}); + + +$DeviceTypeCopyWith<$Res>? get deviceType;$DeviceTypeCopyWith<$Res>? get deviceTypeInfo; + +} +/// @nodoc +class _$DeviceInfoCopyWithImpl<$Res> + implements $DeviceInfoCopyWith<$Res> { + _$DeviceInfoCopyWithImpl(this._self, this._then); + + final DeviceInfo _self; + final $Res Function(DeviceInfo) _then; + +/// 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? 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 +as String,deviceType: freezed == deviceType ? _self.deviceType : deviceType // ignore: cast_nullable_to_non_nullable +as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo : deviceTypeInfo // ignore: cast_nullable_to_non_nullable +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,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?, + )); +} +/// Create a copy of DeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceTypeCopyWith<$Res>? get deviceType { + if (_self.deviceType == null) { + return null; + } + + return $DeviceTypeCopyWith<$Res>(_self.deviceType!, (value) { + return _then(_self.copyWith(deviceType: value)); + }); +}/// Create a copy of DeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceTypeCopyWith<$Res>? get deviceTypeInfo { + if (_self.deviceTypeInfo == null) { + return null; + } + + return $DeviceTypeCopyWith<$Res>(_self.deviceTypeInfo!, (value) { + return _then(_self.copyWith(deviceTypeInfo: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [DeviceInfo]. +extension DeviceInfoPatterns on DeviceInfo { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DeviceInfo value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DeviceInfo() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DeviceInfo value) $default,){ +final _that = this; +switch (_that) { +case _DeviceInfo(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DeviceInfo value)? $default,){ +final _that = this; +switch (_that) { +case _DeviceInfo() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, 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.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, 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.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, 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.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: + return null; + +} +} + +} + +/// @nodoc +@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.firmwareVersion = '', this.lastOnlineAt, this.createdAt}); + factory _DeviceInfo.fromJson(Map json) => _$DeviceInfoFromJson(json); + +@override final int id; +@override final String sn; +@override final DeviceType? deviceType; +@override final DeviceType? deviceTypeInfo; +@override final String? macAddress; +@override@JsonKey() final String name; +@override@JsonKey() final String status; +@override@JsonKey() final String firmwareVersion; +@override final String? lastOnlineAt; +@override final String? createdAt; + +/// Create a copy of DeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DeviceInfoCopyWith<_DeviceInfo> get copyWith => __$DeviceInfoCopyWithImpl<_DeviceInfo>(this, _$identity); + +@override +Map toJson() { + return _$DeviceInfoToJson(this, ); +} + +@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.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,firmwareVersion,lastOnlineAt,createdAt); + +@override +String toString() { + return 'DeviceInfo(id: $id, sn: $sn, deviceType: $deviceType, deviceTypeInfo: $deviceTypeInfo, macAddress: $macAddress, name: $name, status: $status, firmwareVersion: $firmwareVersion, lastOnlineAt: $lastOnlineAt, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$DeviceInfoCopyWith<$Res> implements $DeviceInfoCopyWith<$Res> { + 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, String firmwareVersion, String? lastOnlineAt, String? createdAt +}); + + +@override $DeviceTypeCopyWith<$Res>? get deviceType;@override $DeviceTypeCopyWith<$Res>? get deviceTypeInfo; + +} +/// @nodoc +class __$DeviceInfoCopyWithImpl<$Res> + implements _$DeviceInfoCopyWith<$Res> { + __$DeviceInfoCopyWithImpl(this._self, this._then); + + final _DeviceInfo _self; + final $Res Function(_DeviceInfo) _then; + +/// 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? 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 +as String,deviceType: freezed == deviceType ? _self.deviceType : deviceType // ignore: cast_nullable_to_non_nullable +as DeviceType?,deviceTypeInfo: freezed == deviceTypeInfo ? _self.deviceTypeInfo : deviceTypeInfo // ignore: cast_nullable_to_non_nullable +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,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?, + )); +} + +/// Create a copy of DeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceTypeCopyWith<$Res>? get deviceType { + if (_self.deviceType == null) { + return null; + } + + return $DeviceTypeCopyWith<$Res>(_self.deviceType!, (value) { + return _then(_self.copyWith(deviceType: value)); + }); +}/// Create a copy of DeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceTypeCopyWith<$Res>? get deviceTypeInfo { + if (_self.deviceTypeInfo == null) { + return null; + } + + return $DeviceTypeCopyWith<$Res>(_self.deviceTypeInfo!, (value) { + return _then(_self.copyWith(deviceTypeInfo: value)); + }); +} +} + + +/// @nodoc +mixin _$UserDevice { + + int get id; DeviceInfo get device; int? get spirit; String? get spiritName; String get bindType; String? get bindTime; bool get isActive; +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UserDeviceCopyWith get copyWith => _$UserDeviceCopyWithImpl(this as UserDevice, _$identity); + + /// Serializes this UserDevice to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UserDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.device, device) || other.device == device)&&(identical(other.spirit, spirit) || other.spirit == spirit)&&(identical(other.spiritName, spiritName) || other.spiritName == spiritName)&&(identical(other.bindType, bindType) || other.bindType == bindType)&&(identical(other.bindTime, bindTime) || other.bindTime == bindTime)&&(identical(other.isActive, isActive) || other.isActive == isActive)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,device,spirit,spiritName,bindType,bindTime,isActive); + +@override +String toString() { + return 'UserDevice(id: $id, device: $device, spirit: $spirit, spiritName: $spiritName, bindType: $bindType, bindTime: $bindTime, isActive: $isActive)'; +} + + +} + +/// @nodoc +abstract mixin class $UserDeviceCopyWith<$Res> { + factory $UserDeviceCopyWith(UserDevice value, $Res Function(UserDevice) _then) = _$UserDeviceCopyWithImpl; +@useResult +$Res call({ + int id, DeviceInfo device, int? spirit, String? spiritName, String bindType, String? bindTime, bool isActive +}); + + +$DeviceInfoCopyWith<$Res> get device; + +} +/// @nodoc +class _$UserDeviceCopyWithImpl<$Res> + implements $UserDeviceCopyWith<$Res> { + _$UserDeviceCopyWithImpl(this._self, this._then); + + final UserDevice _self; + final $Res Function(UserDevice) _then; + +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? device = null,Object? spirit = freezed,Object? spiritName = freezed,Object? bindType = null,Object? bindTime = freezed,Object? isActive = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable +as DeviceInfo,spirit: freezed == spirit ? _self.spirit : spirit // ignore: cast_nullable_to_non_nullable +as int?,spiritName: freezed == spiritName ? _self.spiritName : spiritName // ignore: cast_nullable_to_non_nullable +as String?,bindType: null == bindType ? _self.bindType : bindType // ignore: cast_nullable_to_non_nullable +as String,bindTime: freezed == bindTime ? _self.bindTime : bindTime // ignore: cast_nullable_to_non_nullable +as String?,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool, + )); +} +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceInfoCopyWith<$Res> get device { + + return $DeviceInfoCopyWith<$Res>(_self.device, (value) { + return _then(_self.copyWith(device: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [UserDevice]. +extension UserDevicePatterns on UserDevice { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _UserDevice value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _UserDevice() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _UserDevice value) $default,){ +final _that = this; +switch (_that) { +case _UserDevice(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _UserDevice value)? $default,){ +final _that = this; +switch (_that) { +case _UserDevice() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, DeviceInfo device, int? spirit, String? spiritName, String bindType, String? bindTime, bool isActive)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _UserDevice() when $default != null: +return $default(_that.id,_that.device,_that.spirit,_that.spiritName,_that.bindType,_that.bindTime,_that.isActive);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, DeviceInfo device, int? spirit, String? spiritName, String bindType, String? bindTime, bool isActive) $default,) {final _that = this; +switch (_that) { +case _UserDevice(): +return $default(_that.id,_that.device,_that.spirit,_that.spiritName,_that.bindType,_that.bindTime,_that.isActive);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, DeviceInfo device, int? spirit, String? spiritName, String bindType, String? bindTime, bool isActive)? $default,) {final _that = this; +switch (_that) { +case _UserDevice() when $default != null: +return $default(_that.id,_that.device,_that.spirit,_that.spiritName,_that.bindType,_that.bindTime,_that.isActive);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _UserDevice implements UserDevice { + const _UserDevice({required this.id, required this.device, this.spirit, this.spiritName, this.bindType = 'owner', this.bindTime, this.isActive = true}); + factory _UserDevice.fromJson(Map json) => _$UserDeviceFromJson(json); + +@override final int id; +@override final DeviceInfo device; +@override final int? spirit; +@override final String? spiritName; +@override@JsonKey() final String bindType; +@override final String? bindTime; +@override@JsonKey() final bool isActive; + +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$UserDeviceCopyWith<_UserDevice> get copyWith => __$UserDeviceCopyWithImpl<_UserDevice>(this, _$identity); + +@override +Map toJson() { + return _$UserDeviceToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _UserDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.device, device) || other.device == device)&&(identical(other.spirit, spirit) || other.spirit == spirit)&&(identical(other.spiritName, spiritName) || other.spiritName == spiritName)&&(identical(other.bindType, bindType) || other.bindType == bindType)&&(identical(other.bindTime, bindTime) || other.bindTime == bindTime)&&(identical(other.isActive, isActive) || other.isActive == isActive)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,device,spirit,spiritName,bindType,bindTime,isActive); + +@override +String toString() { + return 'UserDevice(id: $id, device: $device, spirit: $spirit, spiritName: $spiritName, bindType: $bindType, bindTime: $bindTime, isActive: $isActive)'; +} + + +} + +/// @nodoc +abstract mixin class _$UserDeviceCopyWith<$Res> implements $UserDeviceCopyWith<$Res> { + factory _$UserDeviceCopyWith(_UserDevice value, $Res Function(_UserDevice) _then) = __$UserDeviceCopyWithImpl; +@override @useResult +$Res call({ + int id, DeviceInfo device, int? spirit, String? spiritName, String bindType, String? bindTime, bool isActive +}); + + +@override $DeviceInfoCopyWith<$Res> get device; + +} +/// @nodoc +class __$UserDeviceCopyWithImpl<$Res> + implements _$UserDeviceCopyWith<$Res> { + __$UserDeviceCopyWithImpl(this._self, this._then); + + final _UserDevice _self; + final $Res Function(_UserDevice) _then; + +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? device = null,Object? spirit = freezed,Object? spiritName = freezed,Object? bindType = null,Object? bindTime = freezed,Object? isActive = null,}) { + return _then(_UserDevice( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable +as DeviceInfo,spirit: freezed == spirit ? _self.spirit : spirit // ignore: cast_nullable_to_non_nullable +as int?,spiritName: freezed == spiritName ? _self.spiritName : spiritName // ignore: cast_nullable_to_non_nullable +as String?,bindType: null == bindType ? _self.bindType : bindType // ignore: cast_nullable_to_non_nullable +as String,bindTime: freezed == bindTime ? _self.bindTime : bindTime // ignore: cast_nullable_to_non_nullable +as String?,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +/// Create a copy of UserDevice +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceInfoCopyWith<$Res> get device { + + return $DeviceInfoCopyWith<$Res>(_self.device, (value) { + return _then(_self.copyWith(device: value)); + }); +} +} + +// dart format on diff --git a/airhub_app/lib/features/device/domain/entities/device.g.dart b/airhub_app/lib/features/device/domain/entities/device.g.dart new file mode 100644 index 0000000..733476b --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_DeviceType _$DeviceTypeFromJson(Map json) => _DeviceType( + id: (json['id'] as num).toInt(), + brand: json['brand'] as String, + productCode: json['product_code'] as String, + name: json['name'] as String, + isNetworkRequired: json['is_network_required'] as bool? ?? true, + isActive: json['is_active'] as bool? ?? true, + createdAt: json['created_at'] as String?, +); + +Map _$DeviceTypeToJson(_DeviceType instance) => + { + 'id': instance.id, + 'brand': instance.brand, + 'product_code': instance.productCode, + 'name': instance.name, + 'is_network_required': instance.isNetworkRequired, + 'is_active': instance.isActive, + 'created_at': instance.createdAt, + }; + +_DeviceInfo _$DeviceInfoFromJson(Map 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), + deviceTypeInfo: json['device_type_info'] == null + ? null + : DeviceType.fromJson(json['device_type_info'] as Map), + macAddress: json['mac_address'] as String?, + name: json['name'] as String? ?? '', + status: json['status'] as String? ?? 'in_stock', + firmwareVersion: json['firmware_version'] as String? ?? '', + lastOnlineAt: json['last_online_at'] as String?, + createdAt: json['created_at'] as String?, +); + +Map _$DeviceInfoToJson(_DeviceInfo instance) => + { + 'id': instance.id, + 'sn': instance.sn, + 'device_type': instance.deviceType, + 'device_type_info': instance.deviceTypeInfo, + 'mac_address': instance.macAddress, + 'name': instance.name, + 'status': instance.status, + 'firmware_version': instance.firmwareVersion, + 'last_online_at': instance.lastOnlineAt, + 'created_at': instance.createdAt, + }; + +_UserDevice _$UserDeviceFromJson(Map json) => _UserDevice( + id: (json['id'] as num).toInt(), + device: DeviceInfo.fromJson(json['device'] as Map), + spirit: (json['spirit'] as num?)?.toInt(), + spiritName: json['spirit_name'] as String?, + bindType: json['bind_type'] as String? ?? 'owner', + bindTime: json['bind_time'] as String?, + isActive: json['is_active'] as bool? ?? true, +); + +Map _$UserDeviceToJson(_UserDevice instance) => + { + 'id': instance.id, + 'device': instance.device, + 'spirit': instance.spirit, + 'spirit_name': instance.spiritName, + 'bind_type': instance.bindType, + 'bind_time': instance.bindTime, + 'is_active': instance.isActive, + }; diff --git a/airhub_app/lib/features/device/domain/entities/device_detail.dart b/airhub_app/lib/features/device/domain/entities/device_detail.dart new file mode 100644 index 0000000..0064cec --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device_detail.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'device_detail.freezed.dart'; +part 'device_detail.g.dart'; + +@freezed +abstract class DeviceDetail with _$DeviceDetail { + const factory DeviceDetail({ + required int id, + required String sn, + @Default('') String name, + @Default('offline') String status, + @Default(0) int battery, + @Default('') String firmwareVersion, + String? macAddress, + @Default(true) bool isAi, + String? icon, + DeviceSettings? settings, + @Default([]) List wifiList, + Map? boundSpirit, + }) = _DeviceDetail; + + factory DeviceDetail.fromJson(Map json) => + _$DeviceDetailFromJson(json); +} + +@freezed +abstract class DeviceSettings with _$DeviceSettings { + const factory DeviceSettings({ + String? nickname, + String? userName, + @Default(50) int volume, + @Default(50) int brightness, + @Default(true) bool allowInterrupt, + @Default(false) bool privacyMode, + }) = _DeviceSettings; + + factory DeviceSettings.fromJson(Map json) => + _$DeviceSettingsFromJson(json); +} + +@freezed +abstract class DeviceWifi with _$DeviceWifi { + const factory DeviceWifi({ + required String ssid, + @Default(false) bool isConnected, + }) = _DeviceWifi; + + factory DeviceWifi.fromJson(Map json) => + _$DeviceWifiFromJson(json); +} diff --git a/airhub_app/lib/features/device/domain/entities/device_detail.freezed.dart b/airhub_app/lib/features/device/domain/entities/device_detail.freezed.dart new file mode 100644 index 0000000..34704da --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device_detail.freezed.dart @@ -0,0 +1,892 @@ +// 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 'device_detail.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$DeviceDetail { + + int get id; String get sn; String get name; String get status; int get battery; String get firmwareVersion; String? get macAddress; bool get isAi; String? get icon; DeviceSettings? get settings; List get wifiList; Map? get boundSpirit; +/// Create a copy of DeviceDetail +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeviceDetailCopyWith get copyWith => _$DeviceDetailCopyWithImpl(this as DeviceDetail, _$identity); + + /// Serializes this DeviceDetail to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceDetail&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.battery, battery) || other.battery == battery)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.isAi, isAi) || other.isAi == isAi)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.settings, settings) || other.settings == settings)&&const DeepCollectionEquality().equals(other.wifiList, wifiList)&&const DeepCollectionEquality().equals(other.boundSpirit, boundSpirit)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,sn,name,status,battery,firmwareVersion,macAddress,isAi,icon,settings,const DeepCollectionEquality().hash(wifiList),const DeepCollectionEquality().hash(boundSpirit)); + +@override +String toString() { + return 'DeviceDetail(id: $id, sn: $sn, name: $name, status: $status, battery: $battery, firmwareVersion: $firmwareVersion, macAddress: $macAddress, isAi: $isAi, icon: $icon, settings: $settings, wifiList: $wifiList, boundSpirit: $boundSpirit)'; +} + + +} + +/// @nodoc +abstract mixin class $DeviceDetailCopyWith<$Res> { + factory $DeviceDetailCopyWith(DeviceDetail value, $Res Function(DeviceDetail) _then) = _$DeviceDetailCopyWithImpl; +@useResult +$Res call({ + int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List wifiList, Map? boundSpirit +}); + + +$DeviceSettingsCopyWith<$Res>? get settings; + +} +/// @nodoc +class _$DeviceDetailCopyWithImpl<$Res> + implements $DeviceDetailCopyWith<$Res> { + _$DeviceDetailCopyWithImpl(this._self, this._then); + + final DeviceDetail _self; + final $Res Function(DeviceDetail) _then; + +/// Create a copy of DeviceDetail +/// 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? name = null,Object? status = null,Object? battery = null,Object? firmwareVersion = null,Object? macAddress = freezed,Object? isAi = null,Object? icon = freezed,Object? settings = freezed,Object? wifiList = null,Object? boundSpirit = 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 +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,battery: null == battery ? _self.battery : battery // ignore: cast_nullable_to_non_nullable +as int,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable +as String,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable +as String?,isAi: null == isAi ? _self.isAi : isAi // ignore: cast_nullable_to_non_nullable +as bool,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable +as String?,settings: freezed == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable +as DeviceSettings?,wifiList: null == wifiList ? _self.wifiList : wifiList // ignore: cast_nullable_to_non_nullable +as List,boundSpirit: freezed == boundSpirit ? _self.boundSpirit : boundSpirit // ignore: cast_nullable_to_non_nullable +as Map?, + )); +} +/// Create a copy of DeviceDetail +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceSettingsCopyWith<$Res>? get settings { + if (_self.settings == null) { + return null; + } + + return $DeviceSettingsCopyWith<$Res>(_self.settings!, (value) { + return _then(_self.copyWith(settings: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [DeviceDetail]. +extension DeviceDetailPatterns on DeviceDetail { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DeviceDetail value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DeviceDetail() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DeviceDetail value) $default,){ +final _that = this; +switch (_that) { +case _DeviceDetail(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DeviceDetail value)? $default,){ +final _that = this; +switch (_that) { +case _DeviceDetail() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List wifiList, Map? boundSpirit)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DeviceDetail() when $default != null: +return $default(_that.id,_that.sn,_that.name,_that.status,_that.battery,_that.firmwareVersion,_that.macAddress,_that.isAi,_that.icon,_that.settings,_that.wifiList,_that.boundSpirit);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List wifiList, Map? boundSpirit) $default,) {final _that = this; +switch (_that) { +case _DeviceDetail(): +return $default(_that.id,_that.sn,_that.name,_that.status,_that.battery,_that.firmwareVersion,_that.macAddress,_that.isAi,_that.icon,_that.settings,_that.wifiList,_that.boundSpirit);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List wifiList, Map? boundSpirit)? $default,) {final _that = this; +switch (_that) { +case _DeviceDetail() when $default != null: +return $default(_that.id,_that.sn,_that.name,_that.status,_that.battery,_that.firmwareVersion,_that.macAddress,_that.isAi,_that.icon,_that.settings,_that.wifiList,_that.boundSpirit);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _DeviceDetail implements DeviceDetail { + const _DeviceDetail({required this.id, required this.sn, this.name = '', this.status = 'offline', this.battery = 0, this.firmwareVersion = '', this.macAddress, this.isAi = true, this.icon, this.settings, final List wifiList = const [], final Map? boundSpirit}): _wifiList = wifiList,_boundSpirit = boundSpirit; + factory _DeviceDetail.fromJson(Map json) => _$DeviceDetailFromJson(json); + +@override final int id; +@override final String sn; +@override@JsonKey() final String name; +@override@JsonKey() final String status; +@override@JsonKey() final int battery; +@override@JsonKey() final String firmwareVersion; +@override final String? macAddress; +@override@JsonKey() final bool isAi; +@override final String? icon; +@override final DeviceSettings? settings; + final List _wifiList; +@override@JsonKey() List get wifiList { + if (_wifiList is EqualUnmodifiableListView) return _wifiList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_wifiList); +} + + final Map? _boundSpirit; +@override Map? get boundSpirit { + final value = _boundSpirit; + if (value == null) return null; + if (_boundSpirit is EqualUnmodifiableMapView) return _boundSpirit; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + + +/// Create a copy of DeviceDetail +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DeviceDetailCopyWith<_DeviceDetail> get copyWith => __$DeviceDetailCopyWithImpl<_DeviceDetail>(this, _$identity); + +@override +Map toJson() { + return _$DeviceDetailToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceDetail&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.battery, battery) || other.battery == battery)&&(identical(other.firmwareVersion, firmwareVersion) || other.firmwareVersion == firmwareVersion)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.isAi, isAi) || other.isAi == isAi)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.settings, settings) || other.settings == settings)&&const DeepCollectionEquality().equals(other._wifiList, _wifiList)&&const DeepCollectionEquality().equals(other._boundSpirit, _boundSpirit)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,sn,name,status,battery,firmwareVersion,macAddress,isAi,icon,settings,const DeepCollectionEquality().hash(_wifiList),const DeepCollectionEquality().hash(_boundSpirit)); + +@override +String toString() { + return 'DeviceDetail(id: $id, sn: $sn, name: $name, status: $status, battery: $battery, firmwareVersion: $firmwareVersion, macAddress: $macAddress, isAi: $isAi, icon: $icon, settings: $settings, wifiList: $wifiList, boundSpirit: $boundSpirit)'; +} + + +} + +/// @nodoc +abstract mixin class _$DeviceDetailCopyWith<$Res> implements $DeviceDetailCopyWith<$Res> { + factory _$DeviceDetailCopyWith(_DeviceDetail value, $Res Function(_DeviceDetail) _then) = __$DeviceDetailCopyWithImpl; +@override @useResult +$Res call({ + int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List wifiList, Map? boundSpirit +}); + + +@override $DeviceSettingsCopyWith<$Res>? get settings; + +} +/// @nodoc +class __$DeviceDetailCopyWithImpl<$Res> + implements _$DeviceDetailCopyWith<$Res> { + __$DeviceDetailCopyWithImpl(this._self, this._then); + + final _DeviceDetail _self; + final $Res Function(_DeviceDetail) _then; + +/// Create a copy of DeviceDetail +/// 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? name = null,Object? status = null,Object? battery = null,Object? firmwareVersion = null,Object? macAddress = freezed,Object? isAi = null,Object? icon = freezed,Object? settings = freezed,Object? wifiList = null,Object? boundSpirit = freezed,}) { + return _then(_DeviceDetail( +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 +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,battery: null == battery ? _self.battery : battery // ignore: cast_nullable_to_non_nullable +as int,firmwareVersion: null == firmwareVersion ? _self.firmwareVersion : firmwareVersion // ignore: cast_nullable_to_non_nullable +as String,macAddress: freezed == macAddress ? _self.macAddress : macAddress // ignore: cast_nullable_to_non_nullable +as String?,isAi: null == isAi ? _self.isAi : isAi // ignore: cast_nullable_to_non_nullable +as bool,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable +as String?,settings: freezed == settings ? _self.settings : settings // ignore: cast_nullable_to_non_nullable +as DeviceSettings?,wifiList: null == wifiList ? _self._wifiList : wifiList // ignore: cast_nullable_to_non_nullable +as List,boundSpirit: freezed == boundSpirit ? _self._boundSpirit : boundSpirit // ignore: cast_nullable_to_non_nullable +as Map?, + )); +} + +/// Create a copy of DeviceDetail +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DeviceSettingsCopyWith<$Res>? get settings { + if (_self.settings == null) { + return null; + } + + return $DeviceSettingsCopyWith<$Res>(_self.settings!, (value) { + return _then(_self.copyWith(settings: value)); + }); +} +} + + +/// @nodoc +mixin _$DeviceSettings { + + String? get nickname; String? get userName; int get volume; int get brightness; bool get allowInterrupt; bool get privacyMode; +/// Create a copy of DeviceSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeviceSettingsCopyWith get copyWith => _$DeviceSettingsCopyWithImpl(this as DeviceSettings, _$identity); + + /// Serializes this DeviceSettings to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceSettings&&(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)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,nickname,userName,volume,brightness,allowInterrupt,privacyMode); + +@override +String toString() { + return 'DeviceSettings(nickname: $nickname, userName: $userName, volume: $volume, brightness: $brightness, allowInterrupt: $allowInterrupt, privacyMode: $privacyMode)'; +} + + +} + +/// @nodoc +abstract mixin class $DeviceSettingsCopyWith<$Res> { + factory $DeviceSettingsCopyWith(DeviceSettings value, $Res Function(DeviceSettings) _then) = _$DeviceSettingsCopyWithImpl; +@useResult +$Res call({ + String? nickname, String? userName, int volume, int brightness, bool allowInterrupt, bool privacyMode +}); + + + + +} +/// @nodoc +class _$DeviceSettingsCopyWithImpl<$Res> + implements $DeviceSettingsCopyWith<$Res> { + _$DeviceSettingsCopyWithImpl(this._self, this._then); + + final DeviceSettings _self; + final $Res Function(DeviceSettings) _then; + +/// Create a copy of DeviceSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? nickname = freezed,Object? userName = freezed,Object? volume = null,Object? brightness = null,Object? allowInterrupt = null,Object? privacyMode = null,}) { + return _then(_self.copyWith( +nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable +as String?,userName: freezed == 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, + )); +} + +} + + +/// Adds pattern-matching-related methods to [DeviceSettings]. +extension DeviceSettingsPatterns on DeviceSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DeviceSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DeviceSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DeviceSettings value) $default,){ +final _that = this; +switch (_that) { +case _DeviceSettings(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DeviceSettings value)? $default,){ +final _that = this; +switch (_that) { +case _DeviceSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? nickname, String? userName, int volume, int brightness, bool allowInterrupt, bool privacyMode)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DeviceSettings() when $default != null: +return $default(_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? nickname, String? userName, int volume, int brightness, bool allowInterrupt, bool privacyMode) $default,) {final _that = this; +switch (_that) { +case _DeviceSettings(): +return $default(_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? nickname, String? userName, int volume, int brightness, bool allowInterrupt, bool privacyMode)? $default,) {final _that = this; +switch (_that) { +case _DeviceSettings() when $default != null: +return $default(_that.nickname,_that.userName,_that.volume,_that.brightness,_that.allowInterrupt,_that.privacyMode);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _DeviceSettings implements DeviceSettings { + const _DeviceSettings({this.nickname, this.userName, this.volume = 50, this.brightness = 50, this.allowInterrupt = true, this.privacyMode = false}); + factory _DeviceSettings.fromJson(Map json) => _$DeviceSettingsFromJson(json); + +@override final String? nickname; +@override final String? userName; +@override@JsonKey() final int volume; +@override@JsonKey() final int brightness; +@override@JsonKey() final bool allowInterrupt; +@override@JsonKey() final bool privacyMode; + +/// Create a copy of DeviceSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DeviceSettingsCopyWith<_DeviceSettings> get copyWith => __$DeviceSettingsCopyWithImpl<_DeviceSettings>(this, _$identity); + +@override +Map toJson() { + return _$DeviceSettingsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceSettings&&(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)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,nickname,userName,volume,brightness,allowInterrupt,privacyMode); + +@override +String toString() { + return 'DeviceSettings(nickname: $nickname, userName: $userName, volume: $volume, brightness: $brightness, allowInterrupt: $allowInterrupt, privacyMode: $privacyMode)'; +} + + +} + +/// @nodoc +abstract mixin class _$DeviceSettingsCopyWith<$Res> implements $DeviceSettingsCopyWith<$Res> { + factory _$DeviceSettingsCopyWith(_DeviceSettings value, $Res Function(_DeviceSettings) _then) = __$DeviceSettingsCopyWithImpl; +@override @useResult +$Res call({ + String? nickname, String? userName, int volume, int brightness, bool allowInterrupt, bool privacyMode +}); + + + + +} +/// @nodoc +class __$DeviceSettingsCopyWithImpl<$Res> + implements _$DeviceSettingsCopyWith<$Res> { + __$DeviceSettingsCopyWithImpl(this._self, this._then); + + final _DeviceSettings _self; + final $Res Function(_DeviceSettings) _then; + +/// Create a copy of DeviceSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? nickname = freezed,Object? userName = freezed,Object? volume = null,Object? brightness = null,Object? allowInterrupt = null,Object? privacyMode = null,}) { + return _then(_DeviceSettings( +nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable +as String?,userName: freezed == 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, + )); +} + + +} + + +/// @nodoc +mixin _$DeviceWifi { + + String get ssid; bool get isConnected; +/// Create a copy of DeviceWifi +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeviceWifiCopyWith get copyWith => _$DeviceWifiCopyWithImpl(this as DeviceWifi, _$identity); + + /// Serializes this DeviceWifi to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceWifi&&(identical(other.ssid, ssid) || other.ssid == ssid)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,ssid,isConnected); + +@override +String toString() { + return 'DeviceWifi(ssid: $ssid, isConnected: $isConnected)'; +} + + +} + +/// @nodoc +abstract mixin class $DeviceWifiCopyWith<$Res> { + factory $DeviceWifiCopyWith(DeviceWifi value, $Res Function(DeviceWifi) _then) = _$DeviceWifiCopyWithImpl; +@useResult +$Res call({ + String ssid, bool isConnected +}); + + + + +} +/// @nodoc +class _$DeviceWifiCopyWithImpl<$Res> + implements $DeviceWifiCopyWith<$Res> { + _$DeviceWifiCopyWithImpl(this._self, this._then); + + final DeviceWifi _self; + final $Res Function(DeviceWifi) _then; + +/// Create a copy of DeviceWifi +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? ssid = null,Object? isConnected = null,}) { + return _then(_self.copyWith( +ssid: null == ssid ? _self.ssid : ssid // ignore: cast_nullable_to_non_nullable +as String,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [DeviceWifi]. +extension DeviceWifiPatterns on DeviceWifi { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DeviceWifi value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DeviceWifi() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DeviceWifi value) $default,){ +final _that = this; +switch (_that) { +case _DeviceWifi(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DeviceWifi value)? $default,){ +final _that = this; +switch (_that) { +case _DeviceWifi() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String ssid, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DeviceWifi() when $default != null: +return $default(_that.ssid,_that.isConnected);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String ssid, bool isConnected) $default,) {final _that = this; +switch (_that) { +case _DeviceWifi(): +return $default(_that.ssid,_that.isConnected);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String ssid, bool isConnected)? $default,) {final _that = this; +switch (_that) { +case _DeviceWifi() when $default != null: +return $default(_that.ssid,_that.isConnected);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _DeviceWifi implements DeviceWifi { + const _DeviceWifi({required this.ssid, this.isConnected = false}); + factory _DeviceWifi.fromJson(Map json) => _$DeviceWifiFromJson(json); + +@override final String ssid; +@override@JsonKey() final bool isConnected; + +/// Create a copy of DeviceWifi +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DeviceWifiCopyWith<_DeviceWifi> get copyWith => __$DeviceWifiCopyWithImpl<_DeviceWifi>(this, _$identity); + +@override +Map toJson() { + return _$DeviceWifiToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeviceWifi&&(identical(other.ssid, ssid) || other.ssid == ssid)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,ssid,isConnected); + +@override +String toString() { + return 'DeviceWifi(ssid: $ssid, isConnected: $isConnected)'; +} + + +} + +/// @nodoc +abstract mixin class _$DeviceWifiCopyWith<$Res> implements $DeviceWifiCopyWith<$Res> { + factory _$DeviceWifiCopyWith(_DeviceWifi value, $Res Function(_DeviceWifi) _then) = __$DeviceWifiCopyWithImpl; +@override @useResult +$Res call({ + String ssid, bool isConnected +}); + + + + +} +/// @nodoc +class __$DeviceWifiCopyWithImpl<$Res> + implements _$DeviceWifiCopyWith<$Res> { + __$DeviceWifiCopyWithImpl(this._self, this._then); + + final _DeviceWifi _self; + final $Res Function(_DeviceWifi) _then; + +/// Create a copy of DeviceWifi +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? ssid = null,Object? isConnected = null,}) { + return _then(_DeviceWifi( +ssid: null == ssid ? _self.ssid : ssid // ignore: cast_nullable_to_non_nullable +as String,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/airhub_app/lib/features/device/domain/entities/device_detail.g.dart b/airhub_app/lib/features/device/domain/entities/device_detail.g.dart new file mode 100644 index 0000000..bc4562f --- /dev/null +++ b/airhub_app/lib/features/device/domain/entities/device_detail.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_detail.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_DeviceDetail _$DeviceDetailFromJson(Map json) => + _DeviceDetail( + id: (json['id'] as num).toInt(), + sn: json['sn'] as String, + name: json['name'] as String? ?? '', + status: json['status'] as String? ?? 'offline', + battery: (json['battery'] as num?)?.toInt() ?? 0, + firmwareVersion: json['firmware_version'] as String? ?? '', + macAddress: json['mac_address'] as String?, + isAi: json['is_ai'] as bool? ?? true, + icon: json['icon'] as String?, + settings: json['settings'] == null + ? null + : DeviceSettings.fromJson(json['settings'] as Map), + wifiList: + (json['wifi_list'] as List?) + ?.map((e) => DeviceWifi.fromJson(e as Map)) + .toList() ?? + const [], + boundSpirit: json['bound_spirit'] as Map?, + ); + +Map _$DeviceDetailToJson(_DeviceDetail instance) => + { + 'id': instance.id, + 'sn': instance.sn, + 'name': instance.name, + 'status': instance.status, + 'battery': instance.battery, + 'firmware_version': instance.firmwareVersion, + 'mac_address': instance.macAddress, + 'is_ai': instance.isAi, + 'icon': instance.icon, + 'settings': instance.settings, + 'wifi_list': instance.wifiList, + 'bound_spirit': instance.boundSpirit, + }; + +_DeviceSettings _$DeviceSettingsFromJson(Map json) => + _DeviceSettings( + 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, + ); + +Map _$DeviceSettingsToJson(_DeviceSettings instance) => + { + 'nickname': instance.nickname, + 'user_name': instance.userName, + 'volume': instance.volume, + 'brightness': instance.brightness, + 'allow_interrupt': instance.allowInterrupt, + 'privacy_mode': instance.privacyMode, + }; + +_DeviceWifi _$DeviceWifiFromJson(Map json) => _DeviceWifi( + ssid: json['ssid'] as String, + isConnected: json['is_connected'] as bool? ?? false, +); + +Map _$DeviceWifiToJson(_DeviceWifi instance) => + { + 'ssid': instance.ssid, + 'is_connected': instance.isConnected, + }; diff --git a/airhub_app/lib/features/device/domain/repositories/device_repository.dart b/airhub_app/lib/features/device/domain/repositories/device_repository.dart new file mode 100644 index 0000000..663446d --- /dev/null +++ b/airhub_app/lib/features/device/domain/repositories/device_repository.dart @@ -0,0 +1,16 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/device.dart'; +import '../entities/device_detail.dart'; + +abstract class DeviceRepository { + Future>> queryByMac(String mac); + Future>> verifyDevice(String sn); + Future> bindDevice(String sn, {int? spiritId}); + Future>> getMyDevices(); + Future> getDeviceDetail(int userDeviceId); + Future> unbindDevice(int userDeviceId); + Future> updateSpirit(int userDeviceId, int spiritId); + Future> updateSettings(int userDeviceId, Map settings); + Future> configWifi(int userDeviceId, String ssid); +} diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart new file mode 100644 index 0000000..1d8c2df --- /dev/null +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart @@ -0,0 +1,106 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../domain/entities/device.dart'; +import '../../domain/entities/device_detail.dart'; +import '../../data/repositories/device_repository_impl.dart'; + +part 'device_controller.g.dart'; + +/// 管理用户设备列表 +@riverpod +class DeviceController extends _$DeviceController { + @override + FutureOr> build() async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.getMyDevices(); + return result.fold( + (failure) => [], + (devices) => devices, + ); + } + + Future bindDevice(String sn, {int? spiritId}) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.bindDevice(sn, spiritId: spiritId); + return result.fold( + (failure) => false, + (userDevice) { + final current = state.value ?? []; + state = AsyncData([...current, userDevice]); + return true; + }, + ); + } + + Future unbindDevice(int userDeviceId) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.unbindDevice(userDeviceId); + return result.fold( + (failure) => false, + (_) { + final current = state.value ?? []; + state = AsyncData( + current.where((d) => d.id != userDeviceId).toList(), + ); + return true; + }, + ); + } + + Future updateSpirit(int userDeviceId, int spiritId) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.updateSpirit(userDeviceId, spiritId); + return result.fold( + (failure) => false, + (updated) { + ref.invalidateSelf(); + return true; + }, + ); + } + + void refresh() { + ref.invalidateSelf(); + } +} + +/// 管理单个设备详情 +@riverpod +class DeviceDetailController extends _$DeviceDetailController { + @override + FutureOr build(int userDeviceId) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.getDeviceDetail(userDeviceId); + return result.fold( + (failure) => null, + (detail) => detail, + ); + } + + Future updateSettings(Map settings) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.updateSettings(userDeviceId, settings); + return result.fold( + (failure) => false, + (_) { + ref.invalidateSelf(); + return true; + }, + ); + } + + Future configWifi(String ssid) async { + final repository = ref.read(deviceRepositoryProvider); + final result = await repository.configWifi(userDeviceId, ssid); + return result.fold( + (failure) => false, + (_) { + ref.invalidateSelf(); + return true; + }, + ); + } + + void refresh() { + ref.invalidateSelf(); + } +} diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart new file mode 100644 index 0000000..05de583 --- /dev/null +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart @@ -0,0 +1,163 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// 管理用户设备列表 + +@ProviderFor(DeviceController) +const deviceControllerProvider = DeviceControllerProvider._(); + +/// 管理用户设备列表 +final class DeviceControllerProvider + extends $AsyncNotifierProvider> { + /// 管理用户设备列表 + const DeviceControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceControllerHash(); + + @$internal + @override + DeviceController create() => DeviceController(); +} + +String _$deviceControllerHash() => r'9b39117bd54964ba0035aad0eca10250454efaa7'; + +/// 管理用户设备列表 + +abstract class _$DeviceController extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// 管理单个设备详情 + +@ProviderFor(DeviceDetailController) +const deviceDetailControllerProvider = DeviceDetailControllerFamily._(); + +/// 管理单个设备详情 +final class DeviceDetailControllerProvider + extends $AsyncNotifierProvider { + /// 管理单个设备详情 + const DeviceDetailControllerProvider._({ + required DeviceDetailControllerFamily super.from, + required int super.argument, + }) : super( + retry: null, + name: r'deviceDetailControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceDetailControllerHash(); + + @override + String toString() { + return r'deviceDetailControllerProvider' + '' + '($argument)'; + } + + @$internal + @override + DeviceDetailController create() => DeviceDetailController(); + + @override + bool operator ==(Object other) { + return other is DeviceDetailControllerProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$deviceDetailControllerHash() => + r'1d9049597e39a0af3a70331378559aca0e1da54d'; + +/// 管理单个设备详情 + +final class DeviceDetailControllerFamily extends $Family + with + $ClassFamilyOverride< + DeviceDetailController, + AsyncValue, + DeviceDetail?, + FutureOr, + int + > { + const DeviceDetailControllerFamily._() + : super( + retry: null, + name: r'deviceDetailControllerProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// 管理单个设备详情 + + DeviceDetailControllerProvider call(int userDeviceId) => + DeviceDetailControllerProvider._(argument: userDeviceId, from: this); + + @override + String toString() => r'deviceDetailControllerProvider'; +} + +/// 管理单个设备详情 + +abstract class _$DeviceDetailController extends $AsyncNotifier { + late final _$args = ref.$arg as int; + int get userDeviceId => _$args; + + FutureOr build(int userDeviceId); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args); + final ref = this.ref as $Ref, DeviceDetail?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, DeviceDetail?>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.dart b/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.dart new file mode 100644 index 0000000..fe8b411 --- /dev/null +++ b/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.dart @@ -0,0 +1,74 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; +import '../../domain/entities/app_notification.dart'; + +part 'notification_remote_data_source.g.dart'; + +abstract class NotificationRemoteDataSource { + /// GET /notifications/?type=xxx&page=1&page_size=20 + Future<({int total, int unreadCount, List items})> + listNotifications({String? type, int page = 1, int pageSize = 20}); + + /// DELETE /notifications/{id}/ + Future deleteNotification(int id); + + /// POST /notifications/{id}/read/ + Future markAsRead(int id); + + /// POST /notifications/read-all/ + Future markAllAsRead(); +} + +@riverpod +NotificationRemoteDataSource notificationRemoteDataSource(Ref ref) { + final apiClient = ref.watch(apiClientProvider); + return NotificationRemoteDataSourceImpl(apiClient); +} + +class NotificationRemoteDataSourceImpl implements NotificationRemoteDataSource { + final ApiClient _apiClient; + + NotificationRemoteDataSourceImpl(this._apiClient); + + @override + Future<({int total, int unreadCount, List items})> + listNotifications({String? type, int page = 1, int pageSize = 20}) async { + final queryParams = { + 'page': page, + 'page_size': pageSize, + }; + if (type != null) queryParams['type'] = type; + + final data = await _apiClient.get( + '/notifications/', + queryParameters: queryParams, + ); + final map = data as Map; + final items = (map['items'] as List) + .map((e) => AppNotification.fromJson(e as Map)) + .toList(); + + return ( + total: map['total'] as int, + unreadCount: map['unread_count'] as int, + items: items, + ); + } + + @override + Future deleteNotification(int id) async { + await _apiClient.delete('/notifications/$id/'); + } + + @override + Future markAsRead(int id) async { + await _apiClient.post('/notifications/$id/read/'); + } + + @override + Future markAllAsRead() async { + final data = await _apiClient.post('/notifications/read-all/'); + final map = data as Map; + return map['count'] as int; + } +} diff --git a/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.g.dart b/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.g.dart new file mode 100644 index 0000000..ebe9b12 --- /dev/null +++ b/airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notification_remote_data_source.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(notificationRemoteDataSource) +const notificationRemoteDataSourceProvider = + NotificationRemoteDataSourceProvider._(); + +final class NotificationRemoteDataSourceProvider + extends + $FunctionalProvider< + NotificationRemoteDataSource, + NotificationRemoteDataSource, + NotificationRemoteDataSource + > + with $Provider { + const NotificationRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'notificationRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$notificationRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + NotificationRemoteDataSource create(Ref ref) { + return notificationRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(NotificationRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$notificationRemoteDataSourceHash() => + r'4e9f903c888936a1f5ff6367213f079547b47047'; diff --git a/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.dart b/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.dart new file mode 100644 index 0000000..3eed7bb --- /dev/null +++ b/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.dart @@ -0,0 +1,78 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../../../../core/errors/failures.dart'; +import '../../domain/entities/app_notification.dart'; +import '../../domain/repositories/notification_repository.dart'; +import '../datasources/notification_remote_data_source.dart'; + +part 'notification_repository_impl.g.dart'; + +@riverpod +NotificationRepository notificationRepository(Ref ref) { + final remoteDataSource = ref.watch(notificationRemoteDataSourceProvider); + return NotificationRepositoryImpl(remoteDataSource); +} + +class NotificationRepositoryImpl implements NotificationRepository { + final NotificationRemoteDataSource _remoteDataSource; + + NotificationRepositoryImpl(this._remoteDataSource); + + @override + Future items})>> + listNotifications({ + String? type, + int page = 1, + int pageSize = 20, + }) async { + try { + final result = await _remoteDataSource.listNotifications( + type: type, + page: page, + pageSize: pageSize, + ); + return right(result); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> deleteNotification(int id) async { + try { + await _remoteDataSource.deleteNotification(id); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> markAsRead(int id) async { + try { + await _remoteDataSource.markAsRead(id); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> markAllAsRead() async { + try { + final count = await _remoteDataSource.markAllAsRead(); + return right(count); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } +} diff --git a/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.g.dart b/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.g.dart new file mode 100644 index 0000000..fa25848 --- /dev/null +++ b/airhub_app/lib/features/notification/data/repositories/notification_repository_impl.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notification_repository_impl.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(notificationRepository) +const notificationRepositoryProvider = NotificationRepositoryProvider._(); + +final class NotificationRepositoryProvider + extends + $FunctionalProvider< + NotificationRepository, + NotificationRepository, + NotificationRepository + > + with $Provider { + const NotificationRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'notificationRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$notificationRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + NotificationRepository create(Ref ref) { + return notificationRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(NotificationRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$notificationRepositoryHash() => + r'ecfecb73514b4e3713b54be327ce323b398df355'; diff --git a/airhub_app/lib/features/notification/domain/entities/app_notification.dart b/airhub_app/lib/features/notification/domain/entities/app_notification.dart new file mode 100644 index 0000000..cd983f6 --- /dev/null +++ b/airhub_app/lib/features/notification/domain/entities/app_notification.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'app_notification.freezed.dart'; +part 'app_notification.g.dart'; + +@freezed +abstract class AppNotification with _$AppNotification { + const factory AppNotification({ + required int id, + required String type, // system, device, activity + required String title, + @Default('') String description, + @Default('') String content, + @Default('') String imageUrl, + @Default(false) bool isRead, + required String createdAt, + }) = _AppNotification; + + factory AppNotification.fromJson(Map json) => + _$AppNotificationFromJson(json); +} diff --git a/airhub_app/lib/features/notification/domain/entities/app_notification.freezed.dart b/airhub_app/lib/features/notification/domain/entities/app_notification.freezed.dart new file mode 100644 index 0000000..42d7700 --- /dev/null +++ b/airhub_app/lib/features/notification/domain/entities/app_notification.freezed.dart @@ -0,0 +1,300 @@ +// 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 'app_notification.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$AppNotification { + + int get id; String get type;// system, device, activity + String get title; String get description; String get content; String get imageUrl; bool get isRead; String get createdAt; +/// Create a copy of AppNotification +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AppNotificationCopyWith get copyWith => _$AppNotificationCopyWithImpl(this as AppNotification, _$identity); + + /// Serializes this AppNotification to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AppNotification&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.isRead, isRead) || other.isRead == isRead)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,type,title,description,content,imageUrl,isRead,createdAt); + +@override +String toString() { + return 'AppNotification(id: $id, type: $type, title: $title, description: $description, content: $content, imageUrl: $imageUrl, isRead: $isRead, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +abstract mixin class $AppNotificationCopyWith<$Res> { + factory $AppNotificationCopyWith(AppNotification value, $Res Function(AppNotification) _then) = _$AppNotificationCopyWithImpl; +@useResult +$Res call({ + int id, String type, String title, String description, String content, String imageUrl, bool isRead, String createdAt +}); + + + + +} +/// @nodoc +class _$AppNotificationCopyWithImpl<$Res> + implements $AppNotificationCopyWith<$Res> { + _$AppNotificationCopyWithImpl(this._self, this._then); + + final AppNotification _self; + final $Res Function(AppNotification) _then; + +/// Create a copy of AppNotification +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? title = null,Object? description = null,Object? content = null,Object? imageUrl = null,Object? isRead = null,Object? createdAt = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable +as String,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,isRead: null == isRead ? _self.isRead : isRead // ignore: cast_nullable_to_non_nullable +as bool,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AppNotification]. +extension AppNotificationPatterns on AppNotification { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AppNotification value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AppNotification() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AppNotification value) $default,){ +final _that = this; +switch (_that) { +case _AppNotification(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AppNotification value)? $default,){ +final _that = this; +switch (_that) { +case _AppNotification() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String type, String title, String description, String content, String imageUrl, bool isRead, String createdAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AppNotification() when $default != null: +return $default(_that.id,_that.type,_that.title,_that.description,_that.content,_that.imageUrl,_that.isRead,_that.createdAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, String type, String title, String description, String content, String imageUrl, bool isRead, String createdAt) $default,) {final _that = this; +switch (_that) { +case _AppNotification(): +return $default(_that.id,_that.type,_that.title,_that.description,_that.content,_that.imageUrl,_that.isRead,_that.createdAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String type, String title, String description, String content, String imageUrl, bool isRead, String createdAt)? $default,) {final _that = this; +switch (_that) { +case _AppNotification() when $default != null: +return $default(_that.id,_that.type,_that.title,_that.description,_that.content,_that.imageUrl,_that.isRead,_that.createdAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _AppNotification implements AppNotification { + const _AppNotification({required this.id, required this.type, required this.title, this.description = '', this.content = '', this.imageUrl = '', this.isRead = false, required this.createdAt}); + factory _AppNotification.fromJson(Map json) => _$AppNotificationFromJson(json); + +@override final int id; +@override final String type; +// system, device, activity +@override final String title; +@override@JsonKey() final String description; +@override@JsonKey() final String content; +@override@JsonKey() final String imageUrl; +@override@JsonKey() final bool isRead; +@override final String createdAt; + +/// Create a copy of AppNotification +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AppNotificationCopyWith<_AppNotification> get copyWith => __$AppNotificationCopyWithImpl<_AppNotification>(this, _$identity); + +@override +Map toJson() { + return _$AppNotificationToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppNotification&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.isRead, isRead) || other.isRead == isRead)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,type,title,description,content,imageUrl,isRead,createdAt); + +@override +String toString() { + return 'AppNotification(id: $id, type: $type, title: $title, description: $description, content: $content, imageUrl: $imageUrl, isRead: $isRead, createdAt: $createdAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$AppNotificationCopyWith<$Res> implements $AppNotificationCopyWith<$Res> { + factory _$AppNotificationCopyWith(_AppNotification value, $Res Function(_AppNotification) _then) = __$AppNotificationCopyWithImpl; +@override @useResult +$Res call({ + int id, String type, String title, String description, String content, String imageUrl, bool isRead, String createdAt +}); + + + + +} +/// @nodoc +class __$AppNotificationCopyWithImpl<$Res> + implements _$AppNotificationCopyWith<$Res> { + __$AppNotificationCopyWithImpl(this._self, this._then); + + final _AppNotification _self; + final $Res Function(_AppNotification) _then; + +/// Create a copy of AppNotification +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? title = null,Object? description = null,Object? content = null,Object? imageUrl = null,Object? isRead = null,Object? createdAt = null,}) { + return _then(_AppNotification( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable +as String,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String,isRead: null == isRead ? _self.isRead : isRead // ignore: cast_nullable_to_non_nullable +as bool,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/airhub_app/lib/features/notification/domain/entities/app_notification.g.dart b/airhub_app/lib/features/notification/domain/entities/app_notification.g.dart new file mode 100644 index 0000000..24b2dfe --- /dev/null +++ b/airhub_app/lib/features/notification/domain/entities/app_notification.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_notification.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_AppNotification _$AppNotificationFromJson(Map json) => + _AppNotification( + id: (json['id'] as num).toInt(), + type: json['type'] as String, + title: json['title'] as String, + description: json['description'] as String? ?? '', + content: json['content'] as String? ?? '', + imageUrl: json['image_url'] as String? ?? '', + isRead: json['is_read'] as bool? ?? false, + createdAt: json['created_at'] as String, + ); + +Map _$AppNotificationToJson(_AppNotification instance) => + { + 'id': instance.id, + 'type': instance.type, + 'title': instance.title, + 'description': instance.description, + 'content': instance.content, + 'image_url': instance.imageUrl, + 'is_read': instance.isRead, + 'created_at': instance.createdAt, + }; diff --git a/airhub_app/lib/features/notification/domain/repositories/notification_repository.dart b/airhub_app/lib/features/notification/domain/repositories/notification_repository.dart new file mode 100644 index 0000000..f71d324 --- /dev/null +++ b/airhub_app/lib/features/notification/domain/repositories/notification_repository.dart @@ -0,0 +1,11 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/app_notification.dart'; + +abstract class NotificationRepository { + Future items})>> + listNotifications({String? type, int page, int pageSize}); + Future> deleteNotification(int id); + Future> markAsRead(int id); + Future> markAllAsRead(); +} diff --git a/airhub_app/lib/features/notification/presentation/controllers/notification_controller.dart b/airhub_app/lib/features/notification/presentation/controllers/notification_controller.dart new file mode 100644 index 0000000..a55543c --- /dev/null +++ b/airhub_app/lib/features/notification/presentation/controllers/notification_controller.dart @@ -0,0 +1,75 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../domain/entities/app_notification.dart'; +import '../../data/repositories/notification_repository_impl.dart'; + +part 'notification_controller.g.dart'; + +@riverpod +class NotificationController extends _$NotificationController { + int _unreadCount = 0; + + int get unreadCount => _unreadCount; + + @override + FutureOr> build() async { + final repository = ref.read(notificationRepositoryProvider); + final result = await repository.listNotifications(); + return result.fold( + (failure) => [], + (data) { + _unreadCount = data.unreadCount; + return data.items; + }, + ); + } + + Future markAsRead(int id) async { + final repository = ref.read(notificationRepositoryProvider); + final result = await repository.markAsRead(id); + return result.fold( + (failure) => false, + (_) { + // Update local state + final current = state.value ?? []; + state = AsyncData( + current.map((n) => n.id == id ? n.copyWith(isRead: true) : n).toList(), + ); + if (_unreadCount > 0) _unreadCount--; + return true; + }, + ); + } + + Future markAllAsRead() async { + final repository = ref.read(notificationRepositoryProvider); + final result = await repository.markAllAsRead(); + return result.fold( + (failure) => false, + (count) { + final current = state.value ?? []; + state = AsyncData( + current.map((n) => n.copyWith(isRead: true)).toList(), + ); + _unreadCount = 0; + return true; + }, + ); + } + + Future deleteNotification(int id) async { + final repository = ref.read(notificationRepositoryProvider); + final result = await repository.deleteNotification(id); + return result.fold( + (failure) => false, + (_) { + final current = state.value ?? []; + state = AsyncData(current.where((n) => n.id != id).toList()); + return true; + }, + ); + } + + void refresh() { + ref.invalidateSelf(); + } +} diff --git a/airhub_app/lib/features/notification/presentation/controllers/notification_controller.g.dart b/airhub_app/lib/features/notification/presentation/controllers/notification_controller.g.dart new file mode 100644 index 0000000..56aadc8 --- /dev/null +++ b/airhub_app/lib/features/notification/presentation/controllers/notification_controller.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notification_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(NotificationController) +const notificationControllerProvider = NotificationControllerProvider._(); + +final class NotificationControllerProvider + extends + $AsyncNotifierProvider> { + const NotificationControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'notificationControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$notificationControllerHash(); + + @$internal + @override + NotificationController create() => NotificationController(); +} + +String _$notificationControllerHash() => + r'99d80e49eb8e81fbae49f9f06f666ef934326a67'; + +abstract class _$NotificationController + extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref + as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue>, + List + >, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.dart b/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.dart new file mode 100644 index 0000000..a5c990a --- /dev/null +++ b/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.dart @@ -0,0 +1,71 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; +import '../../domain/entities/spirit.dart'; + +part 'spirit_remote_data_source.g.dart'; + +abstract class SpiritRemoteDataSource { + Future> listSpirits(); + Future createSpirit(Map data); + Future getSpirit(int id); + Future updateSpirit(int id, Map data); + Future deleteSpirit(int id); + Future unbindSpirit(int id); + Future injectSpirit(int id, int userDeviceId); +} + +@riverpod +SpiritRemoteDataSource spiritRemoteDataSource(Ref ref) { + final apiClient = ref.watch(apiClientProvider); + return SpiritRemoteDataSourceImpl(apiClient); +} + +class SpiritRemoteDataSourceImpl implements SpiritRemoteDataSource { + final ApiClient _apiClient; + + SpiritRemoteDataSourceImpl(this._apiClient); + + @override + Future> listSpirits() async { + final data = await _apiClient.get('/spirits/'); + final list = data as List; + return list + .map((e) => Spirit.fromJson(e as Map)) + .toList(); + } + + @override + Future createSpirit(Map data) async { + final result = await _apiClient.post('/spirits/', data: data); + return Spirit.fromJson(result as Map); + } + + @override + Future getSpirit(int id) async { + final data = await _apiClient.get('/spirits/$id/'); + return Spirit.fromJson(data as Map); + } + + @override + Future updateSpirit(int id, Map data) async { + final result = await _apiClient.put('/spirits/$id/', data: data); + return Spirit.fromJson(result as Map); + } + + @override + Future deleteSpirit(int id) async { + await _apiClient.delete('/spirits/$id/'); + } + + @override + Future unbindSpirit(int id) async { + await _apiClient.post('/spirits/$id/unbind/'); + } + + @override + Future injectSpirit(int id, int userDeviceId) async { + await _apiClient.post('/spirits/$id/inject/', data: { + 'user_device_id': userDeviceId, + }); + } +} diff --git a/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.g.dart b/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.g.dart new file mode 100644 index 0000000..c3211e6 --- /dev/null +++ b/airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spirit_remote_data_source.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(spiritRemoteDataSource) +const spiritRemoteDataSourceProvider = SpiritRemoteDataSourceProvider._(); + +final class SpiritRemoteDataSourceProvider + extends + $FunctionalProvider< + SpiritRemoteDataSource, + SpiritRemoteDataSource, + SpiritRemoteDataSource + > + with $Provider { + const SpiritRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'spiritRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$spiritRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + SpiritRemoteDataSource create(Ref ref) { + return spiritRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SpiritRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$spiritRemoteDataSourceHash() => + r'd968cc481ea0216cb82b898a1ea926094f8ee8f4'; diff --git a/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.dart b/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.dart new file mode 100644 index 0000000..07055ed --- /dev/null +++ b/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.dart @@ -0,0 +1,116 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../../../../core/errors/failures.dart'; +import '../../domain/entities/spirit.dart'; +import '../../domain/repositories/spirit_repository.dart'; +import '../datasources/spirit_remote_data_source.dart'; + +part 'spirit_repository_impl.g.dart'; + +@riverpod +SpiritRepository spiritRepository(Ref ref) { + final remoteDataSource = ref.watch(spiritRemoteDataSourceProvider); + return SpiritRepositoryImpl(remoteDataSource); +} + +class SpiritRepositoryImpl implements SpiritRepository { + final SpiritRemoteDataSource _remoteDataSource; + + SpiritRepositoryImpl(this._remoteDataSource); + + @override + Future>> listSpirits() async { + try { + final spirits = await _remoteDataSource.listSpirits(); + return right(spirits); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> createSpirit({ + required String name, + String? avatar, + String? prompt, + String? memory, + String? voiceId, + }) async { + try { + final data = {'name': name}; + if (avatar != null) data['avatar'] = avatar; + if (prompt != null) data['prompt'] = prompt; + if (memory != null) data['memory'] = memory; + if (voiceId != null) data['voice_id'] = voiceId; + final spirit = await _remoteDataSource.createSpirit(data); + return right(spirit); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> getSpirit(int id) async { + try { + final spirit = await _remoteDataSource.getSpirit(id); + return right(spirit); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> updateSpirit(int id, Map data) async { + try { + final spirit = await _remoteDataSource.updateSpirit(id, data); + return right(spirit); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> deleteSpirit(int id) async { + try { + await _remoteDataSource.deleteSpirit(id); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> unbindSpirit(int id) async { + try { + await _remoteDataSource.unbindSpirit(id); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> injectSpirit(int id, int userDeviceId) async { + try { + await _remoteDataSource.injectSpirit(id, userDeviceId); + return right(null); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } +} diff --git a/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.g.dart b/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.g.dart new file mode 100644 index 0000000..94d9b77 --- /dev/null +++ b/airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spirit_repository_impl.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(spiritRepository) +const spiritRepositoryProvider = SpiritRepositoryProvider._(); + +final class SpiritRepositoryProvider + extends + $FunctionalProvider< + SpiritRepository, + SpiritRepository, + SpiritRepository + > + with $Provider { + const SpiritRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'spiritRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$spiritRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + SpiritRepository create(Ref ref) { + return spiritRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SpiritRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$spiritRepositoryHash() => r'c8ba175770cb9a4aa04b60abac3276ac87f17bc1'; diff --git a/airhub_app/lib/features/spirit/domain/entities/spirit.dart b/airhub_app/lib/features/spirit/domain/entities/spirit.dart new file mode 100644 index 0000000..827bc4a --- /dev/null +++ b/airhub_app/lib/features/spirit/domain/entities/spirit.dart @@ -0,0 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'spirit.freezed.dart'; +part 'spirit.g.dart'; + +@freezed +abstract class Spirit with _$Spirit { + const factory Spirit({ + required int id, + required String name, + String? avatar, + String? prompt, + String? memory, + String? voiceId, + @Default(true) bool isActive, + String? createdAt, + String? updatedAt, + }) = _Spirit; + + factory Spirit.fromJson(Map json) => _$SpiritFromJson(json); +} diff --git a/airhub_app/lib/features/spirit/domain/entities/spirit.freezed.dart b/airhub_app/lib/features/spirit/domain/entities/spirit.freezed.dart new file mode 100644 index 0000000..f3d8ec4 --- /dev/null +++ b/airhub_app/lib/features/spirit/domain/entities/spirit.freezed.dart @@ -0,0 +1,301 @@ +// 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 'spirit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$Spirit { + + int get id; String get name; String? get avatar; String? get prompt; String? get memory; String? get voiceId; bool get isActive; String? get createdAt; String? get updatedAt; +/// Create a copy of Spirit +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SpiritCopyWith get copyWith => _$SpiritCopyWithImpl(this as Spirit, _$identity); + + /// Serializes this Spirit to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Spirit&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.memory, memory) || other.memory == memory)&&(identical(other.voiceId, voiceId) || other.voiceId == voiceId)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(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,name,avatar,prompt,memory,voiceId,isActive,createdAt,updatedAt); + +@override +String toString() { + return 'Spirit(id: $id, name: $name, avatar: $avatar, prompt: $prompt, memory: $memory, voiceId: $voiceId, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SpiritCopyWith<$Res> { + factory $SpiritCopyWith(Spirit value, $Res Function(Spirit) _then) = _$SpiritCopyWithImpl; +@useResult +$Res call({ + int id, String name, String? avatar, String? prompt, String? memory, String? voiceId, bool isActive, String? createdAt, String? updatedAt +}); + + + + +} +/// @nodoc +class _$SpiritCopyWithImpl<$Res> + implements $SpiritCopyWith<$Res> { + _$SpiritCopyWithImpl(this._self, this._then); + + final Spirit _self; + final $Res Function(Spirit) _then; + +/// Create a copy of Spirit +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? avatar = freezed,Object? prompt = freezed,Object? memory = freezed,Object? voiceId = freezed,Object? isActive = 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,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as String?,prompt: freezed == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String?,memory: freezed == memory ? _self.memory : memory // ignore: cast_nullable_to_non_nullable +as String?,voiceId: freezed == voiceId ? _self.voiceId : voiceId // ignore: cast_nullable_to_non_nullable +as String?,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,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 [Spirit]. +extension SpiritPatterns on Spirit { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Spirit value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Spirit() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Spirit value) $default,){ +final _that = this; +switch (_that) { +case _Spirit(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Spirit value)? $default,){ +final _that = this; +switch (_that) { +case _Spirit() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String name, String? avatar, String? prompt, String? memory, String? voiceId, bool isActive, String? createdAt, String? updatedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Spirit() when $default != null: +return $default(_that.id,_that.name,_that.avatar,_that.prompt,_that.memory,_that.voiceId,_that.isActive,_that.createdAt,_that.updatedAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( int id, String name, String? avatar, String? prompt, String? memory, String? voiceId, bool isActive, String? createdAt, String? updatedAt) $default,) {final _that = this; +switch (_that) { +case _Spirit(): +return $default(_that.id,_that.name,_that.avatar,_that.prompt,_that.memory,_that.voiceId,_that.isActive,_that.createdAt,_that.updatedAt);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String name, String? avatar, String? prompt, String? memory, String? voiceId, bool isActive, String? createdAt, String? updatedAt)? $default,) {final _that = this; +switch (_that) { +case _Spirit() when $default != null: +return $default(_that.id,_that.name,_that.avatar,_that.prompt,_that.memory,_that.voiceId,_that.isActive,_that.createdAt,_that.updatedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Spirit implements Spirit { + const _Spirit({required this.id, required this.name, this.avatar, this.prompt, this.memory, this.voiceId, this.isActive = true, this.createdAt, this.updatedAt}); + factory _Spirit.fromJson(Map json) => _$SpiritFromJson(json); + +@override final int id; +@override final String name; +@override final String? avatar; +@override final String? prompt; +@override final String? memory; +@override final String? voiceId; +@override@JsonKey() final bool isActive; +@override final String? createdAt; +@override final String? updatedAt; + +/// Create a copy of Spirit +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SpiritCopyWith<_Spirit> get copyWith => __$SpiritCopyWithImpl<_Spirit>(this, _$identity); + +@override +Map toJson() { + return _$SpiritToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spirit&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.memory, memory) || other.memory == memory)&&(identical(other.voiceId, voiceId) || other.voiceId == voiceId)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(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,name,avatar,prompt,memory,voiceId,isActive,createdAt,updatedAt); + +@override +String toString() { + return 'Spirit(id: $id, name: $name, avatar: $avatar, prompt: $prompt, memory: $memory, voiceId: $voiceId, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SpiritCopyWith<$Res> implements $SpiritCopyWith<$Res> { + factory _$SpiritCopyWith(_Spirit value, $Res Function(_Spirit) _then) = __$SpiritCopyWithImpl; +@override @useResult +$Res call({ + int id, String name, String? avatar, String? prompt, String? memory, String? voiceId, bool isActive, String? createdAt, String? updatedAt +}); + + + + +} +/// @nodoc +class __$SpiritCopyWithImpl<$Res> + implements _$SpiritCopyWith<$Res> { + __$SpiritCopyWithImpl(this._self, this._then); + + final _Spirit _self; + final $Res Function(_Spirit) _then; + +/// Create a copy of Spirit +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? avatar = freezed,Object? prompt = freezed,Object? memory = freezed,Object? voiceId = freezed,Object? isActive = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { + return _then(_Spirit( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable +as String?,prompt: freezed == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String?,memory: freezed == memory ? _self.memory : memory // ignore: cast_nullable_to_non_nullable +as String?,voiceId: freezed == voiceId ? _self.voiceId : voiceId // ignore: cast_nullable_to_non_nullable +as String?,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable +as bool,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/airhub_app/lib/features/spirit/domain/entities/spirit.g.dart b/airhub_app/lib/features/spirit/domain/entities/spirit.g.dart new file mode 100644 index 0000000..8519e62 --- /dev/null +++ b/airhub_app/lib/features/spirit/domain/entities/spirit.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spirit.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_Spirit _$SpiritFromJson(Map json) => _Spirit( + id: (json['id'] as num).toInt(), + name: json['name'] as String, + avatar: json['avatar'] as String?, + prompt: json['prompt'] as String?, + memory: json['memory'] as String?, + voiceId: json['voice_id'] as String?, + isActive: json['is_active'] as bool? ?? true, + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, +); + +Map _$SpiritToJson(_Spirit instance) => { + 'id': instance.id, + 'name': instance.name, + 'avatar': instance.avatar, + 'prompt': instance.prompt, + 'memory': instance.memory, + 'voice_id': instance.voiceId, + 'is_active': instance.isActive, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, +}; diff --git a/airhub_app/lib/features/spirit/domain/repositories/spirit_repository.dart b/airhub_app/lib/features/spirit/domain/repositories/spirit_repository.dart new file mode 100644 index 0000000..c384486 --- /dev/null +++ b/airhub_app/lib/features/spirit/domain/repositories/spirit_repository.dart @@ -0,0 +1,19 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/spirit.dart'; + +abstract class SpiritRepository { + Future>> listSpirits(); + Future> createSpirit({ + required String name, + String? avatar, + String? prompt, + String? memory, + String? voiceId, + }); + Future> getSpirit(int id); + Future> updateSpirit(int id, Map data); + Future> deleteSpirit(int id); + Future> unbindSpirit(int id); + Future> injectSpirit(int id, int userDeviceId); +} diff --git a/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.dart b/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.dart new file mode 100644 index 0000000..ae57e8d --- /dev/null +++ b/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.dart @@ -0,0 +1,85 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../domain/entities/spirit.dart'; +import '../../data/repositories/spirit_repository_impl.dart'; + +part 'spirit_controller.g.dart'; + +@riverpod +class SpiritController extends _$SpiritController { + @override + FutureOr> build() async { + final repository = ref.read(spiritRepositoryProvider); + final result = await repository.listSpirits(); + return result.fold( + (failure) => [], + (spirits) => spirits, + ); + } + + Future create({ + required String name, + String? avatar, + String? prompt, + String? memory, + String? voiceId, + }) async { + final repository = ref.read(spiritRepositoryProvider); + final result = await repository.createSpirit( + name: name, + avatar: avatar, + prompt: prompt, + memory: memory, + voiceId: voiceId, + ); + return result.fold( + (failure) => false, + (spirit) { + final current = state.value ?? []; + state = AsyncData([...current, spirit]); + return true; + }, + ); + } + + Future delete(int id) async { + final repository = ref.read(spiritRepositoryProvider); + final result = await repository.deleteSpirit(id); + return result.fold( + (failure) => false, + (_) { + final current = state.value ?? []; + state = AsyncData(current.where((s) => s.id != id).toList()); + return true; + }, + ); + } + + Future unbind(int id) async { + final repository = ref.read(spiritRepositoryProvider); + final result = await repository.unbindSpirit(id); + return result.fold( + (failure) => false, + (_) { + // 刷新列表以获取最新状态 + ref.invalidateSelf(); + return true; + }, + ); + } + + Future inject(int spiritId, int userDeviceId) async { + final repository = ref.read(spiritRepositoryProvider); + final result = await repository.injectSpirit(spiritId, userDeviceId); + return result.fold( + (failure) => false, + (_) { + ref.invalidateSelf(); + return true; + }, + ); + } + + void refresh() { + ref.invalidateSelf(); + } +} diff --git a/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.g.dart b/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.g.dart new file mode 100644 index 0000000..83160fe --- /dev/null +++ b/airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spirit_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SpiritController) +const spiritControllerProvider = SpiritControllerProvider._(); + +final class SpiritControllerProvider + extends $AsyncNotifierProvider> { + const SpiritControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'spiritControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$spiritControllerHash(); + + @$internal + @override + SpiritController create() => SpiritController(); +} + +String _$spiritControllerHash() => r'fc0837a87a58b59ba7e8c3b92cf448c55c8c508a'; + +abstract class _$SpiritController extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/airhub_app/lib/features/system/data/datasources/system_remote_data_source.dart b/airhub_app/lib/features/system/data/datasources/system_remote_data_source.dart new file mode 100644 index 0000000..54848df --- /dev/null +++ b/airhub_app/lib/features/system/data/datasources/system_remote_data_source.dart @@ -0,0 +1,50 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; + +part 'system_remote_data_source.g.dart'; + +abstract class SystemRemoteDataSource { + /// POST /feedback/ + Future> submitFeedback(String content, {String? contact}); + + /// GET /version/check/?platform=xxx¤t_version=xxx + Future> checkVersion(String platform, String currentVersion); +} + +@riverpod +SystemRemoteDataSource systemRemoteDataSource(Ref ref) { + final apiClient = ref.watch(apiClientProvider); + return SystemRemoteDataSourceImpl(apiClient); +} + +class SystemRemoteDataSourceImpl implements SystemRemoteDataSource { + final ApiClient _apiClient; + + SystemRemoteDataSourceImpl(this._apiClient); + + @override + Future> submitFeedback( + String content, { + String? contact, + }) async { + final body = {'content': content}; + if (contact != null && contact.isNotEmpty) body['contact'] = contact; + final data = await _apiClient.post('/feedback/', data: body); + return data as Map; + } + + @override + Future> checkVersion( + String platform, + String currentVersion, + ) async { + final data = await _apiClient.get( + '/version/check/', + queryParameters: { + 'platform': platform, + 'current_version': currentVersion, + }, + ); + return data as Map; + } +} diff --git a/airhub_app/lib/features/system/data/datasources/system_remote_data_source.g.dart b/airhub_app/lib/features/system/data/datasources/system_remote_data_source.g.dart new file mode 100644 index 0000000..40875dc --- /dev/null +++ b/airhub_app/lib/features/system/data/datasources/system_remote_data_source.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'system_remote_data_source.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(systemRemoteDataSource) +const systemRemoteDataSourceProvider = SystemRemoteDataSourceProvider._(); + +final class SystemRemoteDataSourceProvider + extends + $FunctionalProvider< + SystemRemoteDataSource, + SystemRemoteDataSource, + SystemRemoteDataSource + > + with $Provider { + const SystemRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'systemRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$systemRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + SystemRemoteDataSource create(Ref ref) { + return systemRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SystemRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$systemRemoteDataSourceHash() => + r'ada09ecf278e031e82b96b36b847d4977356d4aa'; diff --git a/airhub_app/lib/features/user/data/datasources/user_remote_data_source.dart b/airhub_app/lib/features/user/data/datasources/user_remote_data_source.dart new file mode 100644 index 0000000..a1df361 --- /dev/null +++ b/airhub_app/lib/features/user/data/datasources/user_remote_data_source.dart @@ -0,0 +1,45 @@ +import 'package:dio/dio.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/network/api_client.dart'; +import '../../../auth/domain/entities/user.dart'; + +part 'user_remote_data_source.g.dart'; + +abstract class UserRemoteDataSource { + Future getMe(); + Future updateMe(Map data); + Future uploadAvatar(String filePath); +} + +@riverpod +UserRemoteDataSource userRemoteDataSource(Ref ref) { + final apiClient = ref.watch(apiClientProvider); + return UserRemoteDataSourceImpl(apiClient); +} + +class UserRemoteDataSourceImpl implements UserRemoteDataSource { + final ApiClient _apiClient; + + UserRemoteDataSourceImpl(this._apiClient); + + @override + Future getMe() async { + final data = await _apiClient.get('/users/me/'); + return User.fromJson(data as Map); + } + + @override + Future updateMe(Map data) async { + final result = await _apiClient.put('/users/update_me/', data: data); + return User.fromJson(result as Map); + } + + @override + Future uploadAvatar(String filePath) async { + final formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(filePath), + }); + final data = await _apiClient.post('/users/avatar/', data: formData); + return (data as Map)['avatar_url'] as String; + } +} diff --git a/airhub_app/lib/features/user/data/datasources/user_remote_data_source.g.dart b/airhub_app/lib/features/user/data/datasources/user_remote_data_source.g.dart new file mode 100644 index 0000000..d80b285 --- /dev/null +++ b/airhub_app/lib/features/user/data/datasources/user_remote_data_source.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_remote_data_source.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(userRemoteDataSource) +const userRemoteDataSourceProvider = UserRemoteDataSourceProvider._(); + +final class UserRemoteDataSourceProvider + extends + $FunctionalProvider< + UserRemoteDataSource, + UserRemoteDataSource, + UserRemoteDataSource + > + with $Provider { + const UserRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + UserRemoteDataSource create(Ref ref) { + return userRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(UserRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$userRemoteDataSourceHash() => + r'61338314bdae7e01a494e565c89fd02ab8d731b7'; diff --git a/airhub_app/lib/features/user/data/repositories/user_repository_impl.dart b/airhub_app/lib/features/user/data/repositories/user_repository_impl.dart new file mode 100644 index 0000000..549849c --- /dev/null +++ b/airhub_app/lib/features/user/data/repositories/user_repository_impl.dart @@ -0,0 +1,65 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../../../../core/errors/failures.dart'; +import '../../../auth/domain/entities/user.dart'; +import '../../domain/repositories/user_repository.dart'; +import '../datasources/user_remote_data_source.dart'; + +part 'user_repository_impl.g.dart'; + +@riverpod +UserRepository userRepository(Ref ref) { + final remoteDataSource = ref.watch(userRemoteDataSourceProvider); + return UserRepositoryImpl(remoteDataSource); +} + +class UserRepositoryImpl implements UserRepository { + final UserRemoteDataSource _remoteDataSource; + + UserRepositoryImpl(this._remoteDataSource); + + @override + Future> getMe() async { + try { + final user = await _remoteDataSource.getMe(); + return right(user); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> updateMe({ + String? nickname, + String? gender, + String? birthday, + }) async { + try { + final data = {}; + if (nickname != null) data['nickname'] = nickname; + if (gender != null) data['gender'] = gender; + if (birthday != null) data['birthday'] = birthday; + final user = await _remoteDataSource.updateMe(data); + return right(user); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } + + @override + Future> uploadAvatar(String filePath) async { + try { + final url = await _remoteDataSource.uploadAvatar(filePath); + return right(url); + } on ServerException catch (e) { + return left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return left(NetworkFailure(e.message)); + } + } +} diff --git a/airhub_app/lib/features/user/data/repositories/user_repository_impl.g.dart b/airhub_app/lib/features/user/data/repositories/user_repository_impl.g.dart new file mode 100644 index 0000000..85a3b9c --- /dev/null +++ b/airhub_app/lib/features/user/data/repositories/user_repository_impl.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_repository_impl.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(userRepository) +const userRepositoryProvider = UserRepositoryProvider._(); + +final class UserRepositoryProvider + extends $FunctionalProvider + with $Provider { + const UserRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + UserRepository create(Ref ref) { + return userRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(UserRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$userRepositoryHash() => r'bcdf0718d6e048bec2e3321db1595c5263baa8d2'; diff --git a/airhub_app/lib/features/user/domain/repositories/user_repository.dart b/airhub_app/lib/features/user/domain/repositories/user_repository.dart new file mode 100644 index 0000000..ae3856d --- /dev/null +++ b/airhub_app/lib/features/user/domain/repositories/user_repository.dart @@ -0,0 +1,13 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/errors/failures.dart'; +import '../../../auth/domain/entities/user.dart'; + +abstract class UserRepository { + Future> getMe(); + Future> updateMe({ + String? nickname, + String? gender, + String? birthday, + }); + Future> uploadAvatar(String filePath); +} diff --git a/airhub_app/lib/features/user/presentation/controllers/user_controller.dart b/airhub_app/lib/features/user/presentation/controllers/user_controller.dart new file mode 100644 index 0000000..ae5d256 --- /dev/null +++ b/airhub_app/lib/features/user/presentation/controllers/user_controller.dart @@ -0,0 +1,64 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../../auth/domain/entities/user.dart'; +import '../../data/repositories/user_repository_impl.dart'; + +part 'user_controller.g.dart'; + +@riverpod +class UserController extends _$UserController { + @override + FutureOr build() async { + final repository = ref.read(userRepositoryProvider); + final result = await repository.getMe(); + return result.fold( + (failure) => null, + (user) => user, + ); + } + + Future updateProfile({ + String? nickname, + String? gender, + String? birthday, + }) async { + final repository = ref.read(userRepositoryProvider); + final result = await repository.updateMe( + nickname: nickname, + gender: gender, + birthday: birthday, + ); + return result.fold( + (failure) { + state = AsyncError(failure.message, StackTrace.current); + return false; + }, + (user) { + state = AsyncData(user); + return true; + }, + ); + } + + Future changeAvatar(String filePath) async { + final repository = ref.read(userRepositoryProvider); + final uploadResult = await repository.uploadAvatar(filePath); + return uploadResult.fold( + (failure) { + state = AsyncError(failure.message, StackTrace.current); + return false; + }, + (avatarUrl) { + // 更新本地用户数据的头像字段 + final currentUser = state.value; + if (currentUser != null) { + state = AsyncData(currentUser.copyWith(avatar: avatarUrl)); + } + return true; + }, + ); + } + + void refresh() { + ref.invalidateSelf(); + } +} diff --git a/airhub_app/lib/features/user/presentation/controllers/user_controller.g.dart b/airhub_app/lib/features/user/presentation/controllers/user_controller.g.dart new file mode 100644 index 0000000..13e11e3 --- /dev/null +++ b/airhub_app/lib/features/user/presentation/controllers/user_controller.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(UserController) +const userControllerProvider = UserControllerProvider._(); + +final class UserControllerProvider + extends $AsyncNotifierProvider { + const UserControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userControllerHash(); + + @$internal + @override + UserController create() => UserController(); +} + +String _$userControllerHash() => r'0f45b9c210e52b75b8a04003b9dbcb08e3e1ed39'; + +abstract class _$UserController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, User?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, User?>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/airhub_app/lib/main.dart b/airhub_app/lib/main.dart index 3a3fa4f..21a7dc8 100644 --- a/airhub_app/lib/main.dart +++ b/airhub_app/lib/main.dart @@ -1,12 +1,41 @@ +import 'dart:async'; +import 'dart:ui' show PlatformDispatcher; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'core/router/app_router.dart'; +import 'core/services/log_center_service.dart'; import 'theme/app_theme.dart'; void main() { - runApp(const ProviderScope(child: AirhubApp())); + runZonedGuarded(() { + WidgetsFlutterBinding.ensureInitialized(); + + final container = ProviderContainer(); + final logCenter = container.read(logCenterServiceProvider); + + // 捕获 Flutter 框架错误(Widget build 异常等) + FlutterError.onError = (details) { + FlutterError.presentError(details); // 保留控制台输出 + logCenter.reportFlutterError(details); + }; + + // 捕获非 Flutter 框架的平台异常 + PlatformDispatcher.instance.onError = (error, stack) { + logCenter.reportUncaughtError(error, stack); + return true; + }; + + runApp(UncontrolledProviderScope( + container: container, + child: const AirhubApp(), + )); + }, (error, stack) { + // Zone 兜底:捕获 runZonedGuarded 区域内的未处理异步异常 + // 此处 container 不可用,直接静态发送 + LogCenterService().reportUncaughtError(error, stack); + }); } class AirhubApp extends ConsumerWidget { diff --git a/airhub_app/lib/pages/bluetooth_page.dart b/airhub_app/lib/pages/bluetooth_page.dart index df17fc8..cde01a0 100644 --- a/airhub_app/lib/pages/bluetooth_page.dart +++ b/airhub_app/lib/pages/bluetooth_page.dart @@ -1,13 +1,19 @@ import 'dart:async'; -import 'dart:math'; +import 'dart:io'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../core/services/ble_provisioning_service.dart'; +import '../features/device/data/datasources/device_remote_data_source.dart'; +import '../features/device/presentation/controllers/device_controller.dart'; import '../widgets/animated_gradient_background.dart'; -import '../theme/app_colors.dart'; import '../widgets/gradient_button.dart'; +import '../widgets/glass_dialog.dart'; /// 设备类型 enum DeviceType { plush, badgeAi, badge } @@ -16,14 +22,20 @@ enum DeviceType { plush, badgeAi, badge } class MockDevice { final String sn; final String name; + final String macAddress; final DeviceType type; final bool hasAI; + final bool isNetworkRequired; + final BluetoothDevice? bleDevice; const MockDevice({ required this.sn, required this.name, + required this.macAddress, required this.type, required this.hasAI, + this.isNetworkRequired = true, + this.bleDevice, }); String get iconPath { @@ -50,53 +62,39 @@ class MockDevice { } /// 蓝牙搜索页面 -class BluetoothPage extends StatefulWidget { +class BluetoothPage extends ConsumerStatefulWidget { const BluetoothPage({super.key}); @override - State createState() => _BluetoothPageState(); + ConsumerState createState() => _BluetoothPageState(); } -class _BluetoothPageState extends State +class _BluetoothPageState extends ConsumerState with TickerProviderStateMixin { + /// Airhub 设备名前缀(硬件广播格式: Airhub_ + MAC) + static const _airhubPrefix = 'Airhub_'; + // 状态 bool _isSearching = true; + bool _isBluetoothOn = false; List _devices = []; int _currentIndex = 0; + // 已查询过的 MAC → 设备信息缓存(避免重复调 API) + final Map> _macInfoCache = {}; + // 动画控制器 late AnimationController _searchAnimController; // 滚轮控制器 late FixedExtentScrollController _wheelController; - // 模拟设备数据 - static const List _mockDevices = [ - MockDevice( - sn: 'PLUSH_01', - name: '卡皮巴拉-001', - type: DeviceType.plush, - hasAI: true, - ), - MockDevice( - sn: 'BADGE_01', - name: 'AI电子吧唧-001', - type: DeviceType.badgeAi, - hasAI: true, - ), - MockDevice( - sn: 'BADGE_02', - name: '电子吧唧-001', - type: DeviceType.badge, - hasAI: false, - ), - MockDevice( - sn: 'PLUSH_02', - name: '卡皮巴拉-002', - type: DeviceType.plush, - hasAI: true, - ), - ]; + // 蓝牙订阅 + StreamSubscription? _bluetoothSubscription; + StreamSubscription>? _scanSubscription; + + // 是否已弹过蓝牙关闭提示(避免重复弹窗) + bool _hasShownBluetoothDialog = false; @override void initState() { @@ -111,61 +109,315 @@ class _BluetoothPageState extends State // 滚轮控制器 _wheelController = FixedExtentScrollController(initialItem: _currentIndex); - // 模拟搜索延迟 - _startSearch(); + // 监听蓝牙适配器状态 + _listenBluetoothState(); } @override void dispose() { + _bluetoothSubscription?.cancel(); + _scanSubscription?.cancel(); + FlutterBluePlus.stopScan(); _searchAnimController.dispose(); _wheelController.dispose(); super.dispose(); } - /// 开始搜索 (模拟) + /// 监听蓝牙适配器状态 + void _listenBluetoothState() { + _bluetoothSubscription = FlutterBluePlus.adapterState.listen((state) { + if (!mounted) return; + + final isOn = state == BluetoothAdapterState.on; + setState(() => _isBluetoothOn = isOn); + + if (isOn) { + _startSearch(); + } else if (state == BluetoothAdapterState.off) { + FlutterBluePlus.stopScan(); + setState(() { + _isSearching = false; + _devices.clear(); + }); + if (!_hasShownBluetoothDialog) { + _hasShownBluetoothDialog = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _showBluetoothOffDialog(); + }); + } + } + }); + } + + /// 从设备名中提取 MAC 地址(格式: Airhub_XXXXXXXXXXXX 或 Airhub_XX:XX:XX:XX:XX:XX) + /// 返回标准格式 XX:XX:XX:XX:XX:XX(大写,带冒号),或 null + String? _extractMacFromName(String bleName) { + if (!bleName.startsWith(_airhubPrefix)) return null; + final rawMac = bleName.substring(_airhubPrefix.length).trim(); + if (rawMac.isEmpty) return null; + + // 移除冒号/横杠,统一处理 + final hex = rawMac.replaceAll(RegExp(r'[:\-]'), '').toUpperCase(); + if (hex.length != 12 || !RegExp(r'^[0-9A-F]{12}$').hasMatch(hex)) { + debugPrint('[BLE Scan] MAC 格式异常: $rawMac'); + return null; + } + + // 转为 XX:XX:XX:XX:XX:XX + return '${hex.substring(0, 2)}:${hex.substring(2, 4)}:${hex.substring(4, 6)}:' + '${hex.substring(6, 8)}:${hex.substring(8, 10)}:${hex.substring(10, 12)}'; + } + + // 暂存扫描到但尚未完成 API 查询的 Airhub 设备 BLE 句柄 + final Map _pendingBleDevices = {}; + + /// 开始 BLE 扫描(持续扫描,直到找到设备并完成 API 查询) Future _startSearch() async { - // 请求蓝牙权限 + if (!_isBluetoothOn) { + _showBluetoothOffDialog(); + return; + } + await _requestPermissions(); - // 模拟 2 秒搜索延迟 - await Future.delayed(const Duration(seconds: 2)); + if (!mounted) return; + setState(() { + _isSearching = true; + _devices.clear(); + _currentIndex = 0; + }); + _pendingBleDevices.clear(); + + _scanSubscription?.cancel(); + _scanSubscription = FlutterBluePlus.onScanResults.listen((results) { + if (!mounted) return; + + for (final r in results) { + final name = r.device.platformName; + if (name.isEmpty) continue; + + final mac = _extractMacFromName(name); + if (mac == null) continue; + + // 记录 BLE 句柄 + _pendingBleDevices[mac] = r.device; + + // 如果没查过这个 MAC,发起 API 查询 + if (!_macInfoCache.containsKey(mac)) { + _macInfoCache[mac] = {}; // 占位,避免重复查询 + _queryDeviceByMac(mac); + } + } + }); + + // 持续扫描(不设超时),由 _queryDeviceByMac 成功后停止 + await FlutterBluePlus.startScan( + timeout: const Duration(seconds: 30), + androidUsesFineLocation: true, + ); + + // 30 秒兜底超时:如果始终没找到设备 + if (mounted && _isSearching) { + setState(() => _isSearching = false); + } + } + + /// 通过 MAC 调用后端 API 查询设备信息 + /// 查询成功后:添加设备到列表、停止扫描、结束搜索状态 + Future _queryDeviceByMac(String mac) async { + try { + final dataSource = ref.read(deviceRemoteDataSourceProvider); + debugPrint('[Bluetooth] queryByMac: $mac'); + final data = await dataSource.queryByMac(mac); + debugPrint('[Bluetooth] queryByMac 返回: $data'); + + if (!mounted) return; + + _macInfoCache[mac] = data; + + final deviceTypeName = data['device_type']?['name'] as String? ?? ''; + final sn = data['sn'] as String? ?? ''; + final isNetworkRequired = data['device_type']?['is_network_required'] as bool? ?? true; + final bleDevice = _pendingBleDevices[mac]; + + // API 返回了有效设备名 → 添加到列表 + final displayName = deviceTypeName.isNotEmpty ? deviceTypeName : 'Airhub 设备'; - if (mounted) { - // 随机选择 1-4 个设备 - final count = Random().nextInt(4) + 1; setState(() { - _devices = _mockDevices.take(count).toList(); + // 避免重复添加 + if (!_devices.any((d) => d.macAddress == mac)) { + _devices.add(MockDevice( + sn: sn, + name: displayName, + macAddress: mac, + type: _inferDeviceType(displayName), + hasAI: _inferHasAI(displayName), + isNetworkRequired: isNetworkRequired, + bleDevice: bleDevice, + )); + } + // 有设备了,结束搜索状态 _isSearching = false; }); + + // 停止扫描 + try { await FlutterBluePlus.stopScan(); } catch (_) {} + + debugPrint('[Bluetooth] 设备已就绪: $mac → $displayName'); + } catch (e) { + debugPrint('[Bluetooth] queryByMac 失败($mac): $e'); + // API 查询失败时,用 BLE 名作为 fallback 也显示出来 + if (!mounted) return; + final bleDevice = _pendingBleDevices[mac]; + setState(() { + if (!_devices.any((d) => d.macAddress == mac)) { + _devices.add(MockDevice( + sn: '', + name: '${_airhubPrefix}设备', + macAddress: mac, + type: DeviceType.plush, + hasAI: true, + bleDevice: bleDevice, + )); + } + _isSearching = false; + }); + try { await FlutterBluePlus.stopScan(); } catch (_) {} } } - /// 请求蓝牙权限(模拟器上可能失败,不影响 mock 搜索) + /// 根据设备名称推断设备类型 + DeviceType _inferDeviceType(String name) { + final lower = name.toLowerCase(); + if (lower.contains('plush') || lower.contains('卡皮巴拉') || lower.contains('机芯') || lower.contains('airhub')) { + return DeviceType.plush; + } + if (lower.contains('ai') || lower.contains('智能')) { + return DeviceType.badgeAi; + } + return DeviceType.badge; + } + + /// 根据设备名称推断是否支持 AI + bool _inferHasAI(String name) { + final lower = name.toLowerCase(); + return lower.contains('ai') || lower.contains('plush') || + lower.contains('卡皮巴拉') || lower.contains('机芯') || lower.contains('智能') || lower.contains('airhub'); + } + + /// 请求蓝牙权限 Future _requestPermissions() async { try { - await Permission.bluetooth.request(); - await Permission.bluetoothScan.request(); - await Permission.bluetoothConnect.request(); - await Permission.location.request(); - } catch (_) { - // 模拟器上蓝牙不可用,忽略权限错误,继续用 mock 数据 + if (Platform.isAndroid) { + // Android 需要位置权限才能扫描 BLE + await Permission.bluetoothScan.request(); + await Permission.bluetoothConnect.request(); + await Permission.location.request(); + } else { + // iOS 只需蓝牙权限,不需要位置 + await Permission.bluetooth.request(); + } + } catch (e) { + debugPrint('[Bluetooth] 权限请求异常: $e'); } } + /// 蓝牙未开启弹窗 + void _showBluetoothOffDialog() { + if (!mounted) return; + showGlassDialog( + context: context, + title: '蓝牙未开启', + description: '请开启蓝牙以搜索附近的设备', + cancelText: '取消', + confirmText: Platform.isAndroid ? '开启蓝牙' : '去设置', + onConfirm: () { + Navigator.of(context).pop(); + if (Platform.isAndroid) { + // Android 可直接请求开启蓝牙 + FlutterBluePlus.turnOn(); + } else { + // iOS 无法直接开启,引导到系统设置 + openAppSettings(); + } + }, + ); + } + + bool _isConnecting = false; + /// 连接设备 - void _handleConnect() { - if (_devices.isEmpty) return; + Future _handleConnect() async { + if (_devices.isEmpty || _isConnecting) return; + + // 检查蓝牙状态 + if (!_isBluetoothOn) { + _showBluetoothOffDialog(); + return; + } final device = _devices[_currentIndex]; - // TODO: 保存设备信息到本地存储 + debugPrint('[Bluetooth] 连接设备: sn=${device.sn}, mac=${device.macAddress}, needsNetwork=${device.isNetworkRequired}'); - if (device.type == DeviceType.badge) { - // 普通吧唧 -> 设备控制页 + if (!device.isNetworkRequired) { + // 不需要联网 -> 直接去设备控制页 context.go('/device-control'); - } else { - // 其他 -> WiFi 配网页 - context.go('/wifi-config'); + return; } + + // Web 环境:跳过 BLE 和 WiFi 配网,直接绑定设备 + if (kIsWeb) { + debugPrint('[Bluetooth] Web 环境,直接绑定 sn=${device.sn}'); + setState(() => _isConnecting = true); + if (device.sn.isNotEmpty) { + await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn); + } + if (!mounted) return; + setState(() => _isConnecting = false); + context.go('/device-control'); + return; + } + + // 需要联网 -> BLE 连接后进入 WiFi 配网 + final bleDevice = device.bleDevice; + if (bleDevice == null) { + debugPrint('[Bluetooth] 无 BLE 句柄,无法连接'); + return; + } + + setState(() => _isConnecting = true); + + // 连接前先停止扫描(iOS 上扫描和连接并发会冲突) + try { + await FlutterBluePlus.stopScan(); + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 300)); + + final provService = BleProvisioningService(); + final ok = await provService.connect(bleDevice); + + if (!mounted) return; + setState(() => _isConnecting = false); + + if (!ok) { + showGlassDialog( + context: context, + title: '连接失败', + description: '无法连接到设备,请确认设备已开机并靠近手机', + confirmText: '确定', + onConfirm: () => Navigator.of(context).pop(), + ); + return; + } + + // BLE 连接成功,跳转 WiFi 配网页并传递 service + context.go('/wifi-config', extra: { + 'provService': provService, + 'sn': device.sn, + 'name': device.name, + 'mac': device.macAddress, + 'type': device.type.name, + }); } @override @@ -564,10 +816,10 @@ class _BluetoothPageState extends State if (!_isSearching && _devices.isNotEmpty) ...[ const SizedBox(width: 16), // HTML: gap 16px GradientButton( - text: '连接设备', + text: _isConnecting ? '连接中...' : '连接设备', width: 180, height: 52, - onPressed: _handleConnect, + onPressed: _isConnecting ? null : _handleConnect, ), ], ], diff --git a/airhub_app/lib/pages/device_control_page.dart b/airhub_app/lib/pages/device_control_page.dart index 4ec693a..edccf4b 100644 --- a/airhub_app/lib/pages/device_control_page.dart +++ b/airhub_app/lib/pages/device_control_page.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart' as http; @@ -18,15 +19,16 @@ import '../widgets/dashed_rect.dart'; import '../widgets/ios_toast.dart'; import '../widgets/animated_gradient_background.dart'; import '../widgets/gradient_button.dart'; +import '../features/device/presentation/controllers/device_controller.dart'; -class DeviceControlPage extends StatefulWidget { +class DeviceControlPage extends ConsumerStatefulWidget { const DeviceControlPage({super.key}); @override - State createState() => _DeviceControlPageState(); + ConsumerState createState() => _DeviceControlPageState(); } -class _DeviceControlPageState extends State +class _DeviceControlPageState extends ConsumerState with SingleTickerProviderStateMixin { int _currentIndex = 0; // 0: Home, 1: Story, 2: Music, 3: User @@ -216,71 +218,8 @@ class _DeviceControlPageState extends State // Add Animation Trigger Logic for testing or real use // We'll hook this up to the Generator Modal return value. - // Status Pill - Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.25), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withOpacity(0.4)), - ), - child: Row( - children: [ - // Live Dot - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: const Color(0xFF22C55E), // Green - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: const Color(0xFF22C55E).withOpacity(0.2), - blurRadius: 0, - spreadRadius: 2, - ), - ], - ), - ), - const SizedBox(width: 8), - Text( - '在线', - style: GoogleFonts.dmSans( - fontSize: 13, - fontWeight: FontWeight.w600, - color: const Color(0xFF4B5563), - ), - ), - // Divider - Container( - margin: const EdgeInsets.symmetric(horizontal: 12), - width: 1, - height: 16, - color: Colors.black.withOpacity(0.1), - ), - // Battery - SvgPicture.asset( - 'assets/www/icons/icon-battery-full.svg', - width: 18, - height: 18, - colorFilter: const ColorFilter.mode( - Color(0xFF4B5563), - BlendMode.srcIn, - ), - ), - const SizedBox(width: 4), - Text( - '85%', - style: GoogleFonts.dmSans( - fontSize: 13, - fontWeight: FontWeight.w600, - color: const Color(0xFF4B5563), - ), - ), - ], - ), - ), + // Status Pill — dynamic from device detail + _buildStatusPill(), // Settings Button _buildIconBtn( @@ -338,6 +277,92 @@ class _DeviceControlPageState extends State ); } + Widget _buildStatusPill() { + final devicesAsync = ref.watch(deviceControllerProvider); + final devices = devicesAsync.value ?? []; + final firstDevice = devices.isNotEmpty ? devices.first : null; + + // If we have a device, try to load its detail for status/battery + String statusText = '离线'; + Color dotColor = const Color(0xFF9CA3AF); + String batteryText = '--'; + + if (firstDevice != null) { + final detailAsync = ref.watch( + deviceDetailControllerProvider(firstDevice.id), + ); + final detail = detailAsync.value; + if (detail != null) { + final isOnline = detail.status == 'online'; + statusText = isOnline ? '在线' : '离线'; + dotColor = isOnline ? const Color(0xFF22C55E) : const Color(0xFF9CA3AF); + batteryText = '${detail.battery}%'; + } + } + + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withOpacity(0.4)), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: dotColor.withOpacity(0.2), + blurRadius: 0, + spreadRadius: 2, + ), + ], + ), + ), + const SizedBox(width: 8), + Text( + statusText, + style: GoogleFonts.dmSans( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xFF4B5563), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + width: 1, + height: 16, + color: Colors.black.withOpacity(0.1), + ), + SvgPicture.asset( + 'assets/www/icons/icon-battery-full.svg', + width: 18, + height: 18, + colorFilter: const ColorFilter.mode( + Color(0xFF4B5563), + BlendMode.srcIn, + ), + ), + const SizedBox(width: 4), + Text( + batteryText, + style: GoogleFonts.dmSans( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xFF4B5563), + ), + ), + ], + ), + ); + } + // --- Home View --- Widget _buildHomeView() { return Center( @@ -770,15 +795,6 @@ class _DeviceControlPageState extends State ); } - Widget _buildPlaceholderView(String title) { - return Center( - child: Text( - title, - style: const TextStyle(fontSize: 16, color: Colors.grey), - ), - ); - } - Widget _buildBottomNavBar() { return Center( child: ClipRRect( diff --git a/airhub_app/lib/pages/profile/agent_manage_page.dart b/airhub_app/lib/pages/profile/agent_manage_page.dart index 51cc9ad..7ffef35 100644 --- a/airhub_app/lib/pages/profile/agent_manage_page.dart +++ b/airhub_app/lib/pages/profile/agent_manage_page.dart @@ -1,39 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart'; import 'package:airhub_app/widgets/glass_dialog.dart'; import 'package:airhub_app/widgets/ios_toast.dart'; +import 'package:airhub_app/features/spirit/domain/entities/spirit.dart'; +import 'package:airhub_app/features/spirit/presentation/controllers/spirit_controller.dart'; -class AgentManagePage extends StatefulWidget { +class AgentManagePage extends ConsumerStatefulWidget { const AgentManagePage({super.key}); @override - State createState() => _AgentManagePageState(); + ConsumerState createState() => _AgentManagePageState(); } -class _AgentManagePageState extends State { - // Mock data matching HTML - final List> _agents = [ - { - 'id': 'Airhub_Mem_01', - 'date': '2025/01/15', - 'icon': '🧠', - 'bind': 'Airhub_5G', - 'nickname': '小毛球', - 'status': 'bound', // bound, unbound - }, - { - 'id': 'Airhub_Mem_02', - 'date': '2024/08/22', - 'icon': '🐾', - 'bind': '未绑定设备', - 'nickname': '豆豆', - 'status': 'unbound', - }, - ]; - +class _AgentManagePageState extends ConsumerState { @override Widget build(BuildContext context) { + final spiritsAsync = ref.watch(spiritControllerProvider); + return Scaffold( backgroundColor: Colors.transparent, body: Stack( @@ -43,16 +28,63 @@ class _AgentManagePageState extends State { children: [ _buildHeader(context), Expanded( - child: ListView.builder( - padding: EdgeInsets.only( - top: 20, - left: 20, - right: 20, - bottom: 40 + MediaQuery.of(context).padding.bottom, + child: spiritsAsync.when( + loading: () => const Center( + child: CircularProgressIndicator(color: Colors.white), ), - itemCount: _agents.length, - itemBuilder: (context, index) { - return _buildAgentCard(_agents[index]); + error: (error, _) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '加载失败', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => ref.read(spiritControllerProvider.notifier).refresh(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + '重试', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ), + data: (spirits) { + if (spirits.isEmpty) { + return Center( + child: Text( + '暂无角色记忆', + style: TextStyle( + color: Colors.white.withOpacity(0.6), + fontSize: 16, + ), + ), + ); + } + return ListView.builder( + padding: EdgeInsets.only( + top: 20, + left: 20, + right: 20, + bottom: 40 + MediaQuery.of(context).padding.bottom, + ), + itemCount: spirits.length, + itemBuilder: (context, index) { + return _buildAgentCard(spirits[index]); + }, + ); }, ), ), @@ -127,7 +159,11 @@ class _AgentManagePageState extends State { ); } - Widget _buildAgentCard(Map agent) { + 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), @@ -171,7 +207,7 @@ class _AgentManagePageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - agent['date']!, + dateStr, style: TextStyle( color: Colors.white.withOpacity(0.85), fontSize: 12, @@ -190,10 +226,19 @@ class _AgentManagePageState extends State { borderRadius: BorderRadius.circular(12), ), alignment: Alignment.center, - child: Text( - agent['icon']!, - style: const 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( @@ -201,7 +246,7 @@ class _AgentManagePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - agent['id']!, + spirit.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -221,9 +266,7 @@ class _AgentManagePageState extends State { ], ), const SizedBox(height: 12), - _buildDetailRow('已绑定:', agent['bind']!), - const SizedBox(height: 4), - _buildDetailRow('角色昵称:', agent['nickname']!), + _buildDetailRow('状态:', spirit.isActive ? '活跃' : '未激活'), const SizedBox(height: 12), Container(height: 1, color: Colors.white.withOpacity(0.2)), @@ -232,18 +275,17 @@ class _AgentManagePageState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - if (agent['status'] == 'bound') - _buildActionBtn( - '解绑', - isDanger: true, - onTap: () => _showUnbindDialog(agent['id']!), - ) - else - _buildActionBtn( - '注入设备', - isInject: true, - onTap: () => _showInjectDialog(agent['id']!), - ), + _buildActionBtn( + '解绑', + isDanger: true, + onTap: () => _showUnbindDialog(spirit), + ), + const SizedBox(width: 8), + _buildActionBtn( + '删除', + isDanger: true, + onTap: () => _showDeleteDialog(spirit), + ), ], ), ], @@ -294,7 +336,7 @@ class _AgentManagePageState extends State { Icons.link_off, size: 14, color: AppColors.danger.withOpacity(0.9), - ), // Use icon for visual + ), const SizedBox(width: 4), ] else if (isInject) ...[ Icon(Icons.download, size: 14, color: const Color(0xFFB07D5A)), @@ -316,23 +358,39 @@ class _AgentManagePageState extends State { ); } - void _showUnbindDialog(String id) { + void _showUnbindDialog(Spirit spirit) { showGlassDialog( context: context, title: '确认解绑角色记忆?', description: '解绑后,该角色记忆将与当前设备断开连接,但数据会保留在云端。', cancelText: '取消', confirmText: '确认解绑', - isDanger: - true, // Note: GlassDialog implementation currently doesn't distinct danger style strongly but passed prop - onConfirm: () { + isDanger: true, + onConfirm: () async { Navigator.pop(context); // Close dialog - AppToast.show(context, '已解绑: $id'); + final success = await ref.read(spiritControllerProvider.notifier).unbind(spirit.id); + if (mounted) { + AppToast.show(context, success ? '已解绑: ${spirit.name}' : '解绑失败'); + } }, ); } - void _showInjectDialog(String id) { - AppToast.show(context, '正在查找附近的可用设备以注入: $id'); + void _showDeleteDialog(Spirit spirit) { + showGlassDialog( + context: context, + title: '确认删除角色记忆?', + description: '删除后,该角色记忆数据将无法恢复。', + cancelText: '取消', + confirmText: '确认删除', + isDanger: true, + onConfirm: () async { + Navigator.pop(context); + final success = await ref.read(spiritControllerProvider.notifier).delete(spirit.id); + if (mounted) { + AppToast.show(context, success ? '已删除: ${spirit.name}' : '删除失败'); + } + }, + ); } } diff --git a/airhub_app/lib/pages/profile/notification_page.dart b/airhub_app/lib/pages/profile/notification_page.dart index b141ed3..ee193b5 100644 --- a/airhub_app/lib/pages/profile/notification_page.dart +++ b/airhub_app/lib/pages/profile/notification_page.dart @@ -1,81 +1,40 @@ import 'package:flutter/material.dart'; +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/features/notification/domain/entities/app_notification.dart'; +import 'package:airhub_app/features/notification/presentation/controllers/notification_controller.dart'; -/// 消息通知页面 — 还原 notifications.html -class NotificationPage extends StatefulWidget { +/// 消息通知页面 — 接入真实 API +class NotificationPage extends ConsumerStatefulWidget { const NotificationPage({super.key}); @override - State createState() => _NotificationPageState(); + ConsumerState createState() => _NotificationPageState(); } -class _NotificationPageState extends State { - /// 当前展开的通知 index(-1 表示全部折叠,手风琴模式) - int _expandedIndex = -1; +class _NotificationPageState extends ConsumerState { + /// 当前展开的通知 id(null 表示全部折叠) + int? _expandedId; - /// 已读标记(index set) - final Set _readIndices = {}; - - /// 示例通知数据 - final List<_NotificationData> _notifications = [ - _NotificationData( - type: _NotifType.system, - icon: Icons.warning_amber_rounded, - title: '系统更新', - time: '10:30', - desc: 'Airhub V1.2.0 版本更新已准备就绪', - detail: 'Airhub V1.2.0 版本更新说明:\n\n' - '• 新增"喂养指南"功能,现在您可以查看详细的电子宠物养成手册了。\n' - '• 优化了设备连接的稳定性,修复了部分机型搜索不到设备的问题。\n' - '• 提升了整体界面的流畅度,增加了更多微交互动画。\n\n' - '建议您连接 Wi-Fi 后进行更新,以获得最佳体验。', - isUnread: true, - ), - _NotificationData( - type: _NotifType.activity, - emojiIcon: '🎁', - title: '新春活动', - time: '昨天', - desc: '领取您的新春限定水豚皮肤"招财进宝"', - detail: '🎉 新春限定皮肤上线啦!\n\n' - '为了庆祝即将到来的春节,我们特别推出了水豚的"招财进宝"限定皮肤。\n\n' - '活动亮点:\n' - '• 限定版红色唐装外观\n' - '• 专属的春节互动音效\n' - '• 限时免费领取的节庆道具\n\n' - '活动截止日期:2月15日', - isUnread: false, - ), - _NotificationData( - type: _NotifType.system, - icon: Icons.person_add_alt_1_outlined, - title: '新设备绑定', - time: '1月20日', - desc: '您的新设备"Airhub_5G"已成功绑定', - detail: '恭喜!您已成功绑定新设备 Airhub_5G。\n\n' - '接下来的几步可以帮助您快速上手:\n' - '• 前往角色记忆页面,注入您喜欢的角色人格。\n' - '• 进入设置页面配置您的偏好设置。\n' - '• 查看帮助中心的入门指南,解锁更多互动玩法。\n\n' - '祝您开启一段奇妙的 AI 陪伴旅程!', - isUnread: false, - ), - ]; - - void _toggleNotification(int index) { + void _toggleNotification(AppNotification notif) { setState(() { - if (_expandedIndex == index) { - _expandedIndex = -1; // 折叠 + if (_expandedId == notif.id) { + _expandedId = null; } else { - _expandedIndex = index; // 展开,并标记已读 - _readIndices.add(index); + _expandedId = notif.id; + // Mark as read when expanded + if (!notif.isRead) { + ref.read(notificationControllerProvider.notifier).markAsRead(notif.id); + } } }); } @override Widget build(BuildContext context) { + final notificationsAsync = ref.watch(notificationControllerProvider); + return Scaffold( backgroundColor: Colors.transparent, body: Stack( @@ -85,24 +44,69 @@ class _NotificationPageState extends State { children: [ _buildHeader(context), Expanded( - child: ShaderMask( - shaderCallback: (Rect rect) { - return const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black, - Colors.black, - Colors.transparent, + child: notificationsAsync.when( + loading: () => const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + error: (error, _) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '加载失败', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => ref.read(notificationControllerProvider.notifier).refresh(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text('重试', style: TextStyle(color: Colors.white)), + ), + ), ], - stops: [0.0, 0.03, 0.95, 1.0], - ).createShader(rect); + ), + ), + data: (notifications) { + if (notifications.isEmpty) { + return _buildEmptyState(); + } + return ShaderMask( + shaderCallback: (Rect rect) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black, + Colors.black, + Colors.transparent, + ], + stops: [0.0, 0.03, 0.95, 1.0], + ).createShader(rect); + }, + blendMode: BlendMode.dstIn, + child: ListView.builder( + padding: EdgeInsets.only( + top: 8, + left: 20, + right: 20, + bottom: 40 + MediaQuery.of(context).padding.bottom, + ), + itemCount: notifications.length, + itemBuilder: (context, index) { + return _buildNotificationCard(notifications[index]); + }, + ), + ); }, - blendMode: BlendMode.dstIn, - child: _notifications.isEmpty - ? _buildEmptyState() - : _buildNotificationList(context), ), ), ], @@ -112,7 +116,6 @@ class _NotificationPageState extends State { ); } - // ─── Header ─── Widget _buildHeader(BuildContext context) { return Container( padding: EdgeInsets.only( @@ -141,13 +144,12 @@ class _NotificationPageState extends State { ), ), Text('消息通知', style: AppTextStyles.title), - const SizedBox(width: 40), // 右侧占位保持标题居中 + const SizedBox(width: 40), ], ), ); } - // ─── 空状态 ─── Widget _buildEmptyState() { return Center( child: Column( @@ -156,8 +158,8 @@ class _NotificationPageState extends State { Container( width: 100, height: 100, - decoration: BoxDecoration( - color: const Color(0x1A9CA3AF), + decoration: const BoxDecoration( + color: Color(0x1A9CA3AF), shape: BoxShape.circle, ), child: const Icon( @@ -188,27 +190,10 @@ class _NotificationPageState extends State { ); } - // ─── 通知列表 ─── - Widget _buildNotificationList(BuildContext context) { - return ListView.builder( - padding: EdgeInsets.only( - top: 8, - left: 20, - right: 20, - bottom: 40 + MediaQuery.of(context).padding.bottom, - ), - itemCount: _notifications.length, - itemBuilder: (context, index) { - return _buildNotificationCard(index); - }, - ); - } - - // ─── 单条通知卡片 ─── - Widget _buildNotificationCard(int index) { - final notif = _notifications[index]; - final isExpanded = _expandedIndex == index; - final isUnread = notif.isUnread && !_readIndices.contains(index); + Widget _buildNotificationCard(AppNotification notif) { + final isExpanded = _expandedId == notif.id; + final isUnread = !notif.isRead; + final timeStr = _formatTime(notif.createdAt); return Padding( padding: const EdgeInsets.only(bottom: 12), @@ -217,29 +202,24 @@ class _NotificationPageState extends State { curve: Curves.easeInOut, decoration: BoxDecoration( color: isExpanded - ? const Color(0xD9FFFFFF) // 0.85 opacity - : const Color(0xB3FFFFFF), // 0.7 opacity + ? const Color(0xD9FFFFFF) + : const Color(0xB3FFFFFF), borderRadius: BorderRadius.circular(16), - border: Border.all( - color: const Color(0x66FFFFFF), // rgba(255,255,255,0.4) - ), + border: Border.all(color: const Color(0x66FFFFFF)), boxShadow: const [AppShadows.card], ), child: Column( children: [ - // ── 卡片头部(可点击) ── GestureDetector( - onTap: () => _toggleNotification(index), + onTap: () => _toggleNotification(notif), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 图标 _buildNotifIcon(notif), const SizedBox(width: 14), - // 文字区域 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -247,16 +227,20 @@ class _NotificationPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - notif.title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, + Expanded( + child: Text( + notif.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, ), ), + const SizedBox(width: 8), Text( - notif.time, + timeStr, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, @@ -266,7 +250,7 @@ class _NotificationPageState extends State { ), const SizedBox(height: 4), Text( - notif.desc, + notif.description, style: const TextStyle( fontSize: 13, color: Color(0xFF6B7280), @@ -277,7 +261,6 @@ class _NotificationPageState extends State { ), ), const SizedBox(width: 8), - // 箭头 + 未读红点 Column( children: [ AnimatedRotation( @@ -342,57 +325,71 @@ class _NotificationPageState extends State { ); } - // ─── 通知图标 ─── - Widget _buildNotifIcon(_NotificationData notif) { - final isSystem = notif.type == _NotifType.system; + Widget _buildNotifIcon(AppNotification notif) { + final isSystem = notif.type == 'system'; + final isActivity = notif.type == 'activity'; + + IconData icon; + if (isSystem) { + icon = Icons.info_outline; + } else if (isActivity) { + icon = Icons.card_giftcard; + } else { + icon = Icons.devices; + } + return Container( width: 40, height: 40, decoration: BoxDecoration( color: isSystem - ? const Color(0xFFEFF6FF) // 蓝色系统背景 - : const Color(0xFFFFF7ED), // 橙色活动背景 + ? const Color(0xFFEFF6FF) + : isActivity + ? const Color(0xFFFFF7ED) + : const Color(0xFFF0FDF4), borderRadius: BorderRadius.circular(20), ), alignment: Alignment.center, - child: notif.emojiIcon != null - ? Text( - notif.emojiIcon!, - style: const TextStyle(fontSize: 18), + child: notif.imageUrl.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Image.network( + notif.imageUrl, + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Icon( + icon, + size: 20, + color: isSystem ? const Color(0xFF3B82F6) : const Color(0xFFF97316), + ), + ), ) : Icon( - notif.icon ?? Icons.info_outline, + icon, size: 20, color: isSystem ? const Color(0xFF3B82F6) - : const Color(0xFFF97316), + : isActivity + ? const Color(0xFFF97316) + : const Color(0xFF22C55E), ), ); } -} - -// ─── 数据模型 ─── - -enum _NotifType { system, activity } - -class _NotificationData { - final _NotifType type; - final IconData? icon; - final String? emojiIcon; - final String title; - final String time; - final String desc; - final String detail; - final bool isUnread; - - _NotificationData({ - required this.type, - this.icon, - this.emojiIcon, - required this.title, - required this.time, - required this.desc, - required this.detail, - this.isUnread = false, - }); + + String _formatTime(String createdAt) { + try { + final dt = DateTime.parse(createdAt); + final now = DateTime.now(); + final diff = now.difference(dt); + + if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前'; + if (diff.inHours < 24) return '${diff.inHours}小时前'; + if (diff.inDays < 2) return '昨天'; + if (diff.inDays < 7) return '${diff.inDays}天前'; + return '${dt.month}月${dt.day}日'; + } catch (_) { + return createdAt; + } + } } diff --git a/airhub_app/lib/pages/profile/profile_info_page.dart b/airhub_app/lib/pages/profile/profile_info_page.dart index 0d95495..7c1a16f 100644 --- a/airhub_app/lib/pages/profile/profile_info_page.dart +++ b/airhub_app/lib/pages/profile/profile_info_page.dart @@ -1,35 +1,73 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +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/ios_toast.dart'; +import 'package:airhub_app/features/user/presentation/controllers/user_controller.dart'; import 'package:image_picker/image_picker.dart'; -import 'dart:io'; +import 'dart:typed_data'; -class ProfileInfoPage extends StatefulWidget { +class ProfileInfoPage extends ConsumerStatefulWidget { const ProfileInfoPage({super.key}); @override - State createState() => _ProfileInfoPageState(); + ConsumerState createState() => _ProfileInfoPageState(); } -class _ProfileInfoPageState extends State { - String _gender = '男'; - String _birthday = '1994-12-09'; - File? _avatarImage; - final TextEditingController _nicknameController = TextEditingController( - text: '土豆', - ); +class _ProfileInfoPageState extends ConsumerState { + String _gender = ''; + String _birthday = ''; + Uint8List? _avatarBytes; + String? _avatarUrl; + late final TextEditingController _nicknameController; + bool _initialized = false; + + @override + void initState() { + super.initState(); + _nicknameController = TextEditingController(); + } + + @override + void dispose() { + _nicknameController.dispose(); + super.dispose(); + } + + static const _genderToDisplay = { + 'male': '男', + 'female': '女', + 'M': '男', + 'F': '女', + }; + static const _displayToGender = {'男': 'male', '女': 'female'}; + + void _initFromUser() { + if (_initialized) return; + final userAsync = ref.read(userControllerProvider); + final user = userAsync.value; + if (user != null) { + _nicknameController.text = user.nickname ?? ''; + _gender = _genderToDisplay[user.gender] ?? user.gender ?? ''; + _birthday = user.birthday ?? ''; + _avatarUrl = user.avatar; + _initialized = true; + } + } @override Widget build(BuildContext context) { + final userAsync = ref.watch(userControllerProvider); + + // 首次从用户数据初始化表单 + userAsync.whenData((_) => _initFromUser()); + return Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ - // 动态渐变背景 const AnimatedGradientBackground(), - Column( children: [ _buildHeader(context), @@ -98,11 +136,7 @@ class _ProfileInfoPageState extends State { Widget _buildSaveButton() { return GestureDetector( - onTap: () { - // Save logic - show success toast - AppToast.show(context, '保存成功'); - Navigator.pop(context); - }, + onTap: _saveProfile, child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( @@ -125,6 +159,26 @@ class _ProfileInfoPageState extends State { ); } + Future _saveProfile() async { + // 转换性别为后端格式 + final genderCode = _displayToGender[_gender]; + + final success = await ref.read(userControllerProvider.notifier).updateProfile( + nickname: _nicknameController.text.trim(), + gender: genderCode, + birthday: _birthday.isNotEmpty ? _birthday : null, + ); + + if (mounted) { + if (success) { + AppToast.show(context, '保存成功'); + Navigator.pop(context); + } else { + AppToast.show(context, '保存失败,请重试', isError: true); + } + } + } + Widget _buildAvatarSection() { return Stack( children: [ @@ -140,21 +194,28 @@ class _ProfileInfoPageState extends State { ), boxShadow: [ BoxShadow( - color: Color(0x338B5E3C), // rgba(139, 94, 60, 0.2) + color: Color(0x338B5E3C), blurRadius: 24, offset: Offset(0, 8), ), ], ), child: ClipOval( - child: _avatarImage != null - ? Image.file(_avatarImage!, fit: BoxFit.cover) - : Image.asset( - 'assets/www/Capybara.png', - fit: BoxFit.cover, - errorBuilder: (ctx, err, stack) => - const Icon(Icons.person, color: Colors.white, size: 40), - ), + child: _avatarBytes != null + ? Image.memory(_avatarBytes!, fit: BoxFit.cover) + : (_avatarUrl != null && _avatarUrl!.isNotEmpty) + ? Image.network( + _avatarUrl!, + fit: BoxFit.cover, + errorBuilder: (ctx, err, stack) => + Image.asset('assets/www/Capybara.png', fit: BoxFit.cover), + ) + : Image.asset( + 'assets/www/Capybara.png', + fit: BoxFit.cover, + errorBuilder: (ctx, err, stack) => + const Icon(Icons.person, color: Colors.white, size: 40), + ), ), ), Positioned( @@ -197,13 +258,19 @@ class _ProfileInfoPageState extends State { final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { + final bytes = await image.readAsBytes(); setState(() { - _avatarImage = File(image.path); + _avatarBytes = bytes; }); + // 上传头像到服务器 + final success = await ref.read(userControllerProvider.notifier).changeAvatar(image.path); + if (mounted && !success) { + AppToast.show(context, '头像上传失败', isError: true); + } } } catch (e) { if (mounted) { - AppToast.show(context, '选择图片失败: $e', isError: true); + AppToast.show(context, '选择图片失败', isError: true); } } } @@ -218,10 +285,14 @@ class _ProfileInfoPageState extends State { child: Column( children: [ _buildInputItem('昵称', _nicknameController), - _buildSelectionItem('性别', _gender, onTap: _showGenderModal), + _buildSelectionItem( + '性别', + _gender.isEmpty ? '未设置' : _gender, + onTap: _showGenderModal, + ), _buildSelectionItem( '生日', - _birthday, + _birthday.isEmpty ? '未设置' : _birthday, showDivider: false, onTap: _showBirthdayInput, ), @@ -330,8 +401,8 @@ class _ProfileInfoPageState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Color(0xFFFFFDF9), // warm white top - Color(0xFFFFF8F0), // slightly warmer bottom + Color(0xFFFFFDF9), + Color(0xFFFFF8F0), ], ), borderRadius: BorderRadius.vertical(top: Radius.circular(24)), @@ -347,7 +418,6 @@ class _ProfileInfoPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar Container( width: 36, height: 4, @@ -359,13 +429,10 @@ class _ProfileInfoPageState extends State { const SizedBox(height: 20), Text('选择性别', style: AppTextStyles.title), const SizedBox(height: 24), - // Male option _buildGenderOption('男'), const SizedBox(height: 12), - // Female option _buildGenderOption('女'), const SizedBox(height: 16), - // Cancel GestureDetector( onTap: () => Navigator.pop(context), child: Container( @@ -406,12 +473,12 @@ class _ProfileInfoPageState extends State { padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( color: isSelected - ? const Color(0xFFFFF5EB) // warm selected bg + ? const Color(0xFFFFF5EB) : Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(16), border: Border.all( color: isSelected - ? const Color(0xFFECCFA8) // warm gold border + ? const Color(0xFFECCFA8) : const Color(0xFFE5E7EB), width: isSelected ? 1.5 : 1, ), @@ -434,7 +501,7 @@ class _ProfileInfoPageState extends State { fontSize: 16, fontWeight: FontWeight.w600, color: isSelected - ? const Color(0xFFB07D5A) // warm brown text + ? const Color(0xFFB07D5A) : const Color(0xFF374151), ), ), @@ -444,7 +511,6 @@ class _ProfileInfoPageState extends State { ); } - // iOS-style wheel date picker void _showBirthdayInput() { DateTime tempDate = DateTime.tryParse(_birthday) ?? DateTime(1994, 12, 9); @@ -473,7 +539,6 @@ class _ProfileInfoPageState extends State { ), child: Column( children: [ - // Header Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: const BoxDecoration( @@ -527,7 +592,6 @@ class _ProfileInfoPageState extends State { ], ), ), - // Cupertino date picker wheel Expanded( child: CupertinoTheme( data: const CupertinoThemeData( diff --git a/airhub_app/lib/pages/profile/profile_page.dart b/airhub_app/lib/pages/profile/profile_page.dart index 9ec9db8..7149362 100644 --- a/airhub_app/lib/pages/profile/profile_page.dart +++ b/airhub_app/lib/pages/profile/profile_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/feedback_dialog.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart'; @@ -9,12 +10,15 @@ import 'package:airhub_app/pages/profile/help_page.dart'; import 'package:airhub_app/pages/product_selection_page.dart'; import 'package:airhub_app/pages/profile/notification_page.dart'; import 'package:airhub_app/widgets/ios_toast.dart'; +import 'package:airhub_app/features/user/presentation/controllers/user_controller.dart'; -class ProfilePage extends StatelessWidget { +class ProfilePage extends ConsumerWidget { const ProfilePage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final userAsync = ref.watch(userControllerProvider); + final user = userAsync.value; return Scaffold( backgroundColor: Colors.transparent, body: Stack( @@ -35,7 +39,7 @@ class ProfilePage extends StatelessWidget { children: [ const SizedBox(height: 20), // Top spacing const SizedBox(height: 20), // Top spacing - _buildUserCard(context), + _buildUserCard(context, user), const SizedBox(height: 20), _buildMenuList(context), const SizedBox(height: 140), // Bottom padding for footer @@ -117,7 +121,14 @@ class ProfilePage extends StatelessWidget { ); } - Widget _buildUserCard(BuildContext context) { + Widget _buildUserCard(BuildContext context, dynamic user) { + final nickname = user?.nickname ?? '未设置昵称'; + final phone = user?.phone ?? ''; + final maskedPhone = phone.length >= 7 + ? '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}' + : phone; + final avatarUrl = user?.avatar as String?; + return GestureDetector( onTap: () { Navigator.push( @@ -146,12 +157,19 @@ class ProfilePage extends StatelessWidget { ), ), child: ClipOval( - child: Image.asset( - 'assets/www/Capybara.png', - fit: BoxFit.cover, - errorBuilder: (ctx, err, stack) => - const Icon(Icons.person, color: Colors.white), - ), // Fallback + child: (avatarUrl != null && avatarUrl.isNotEmpty) + ? Image.network( + avatarUrl, + fit: BoxFit.cover, + errorBuilder: (ctx, err, stack) => + Image.asset('assets/www/Capybara.png', fit: BoxFit.cover), + ) + : Image.asset( + 'assets/www/Capybara.png', + fit: BoxFit.cover, + errorBuilder: (ctx, err, stack) => + const Icon(Icons.person, color: Colors.white), + ), ), ), const SizedBox(width: AppSpacing.md), @@ -159,9 +177,9 @@ class ProfilePage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('土豆', style: AppTextStyles.userName), + Text(nickname, style: AppTextStyles.userName), const SizedBox(height: 4), - Text('ID: 138****3069', style: AppTextStyles.userId), + Text('ID: $maskedPhone', style: AppTextStyles.userId), ], ), ), diff --git a/airhub_app/lib/pages/profile/settings_page.dart b/airhub_app/lib/pages/profile/settings_page.dart index 9e2369d..6aa576a 100644 --- a/airhub_app/lib/pages/profile/settings_page.dart +++ b/airhub_app/lib/pages/profile/settings_page.dart @@ -1,19 +1,24 @@ +import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/animated_gradient_background.dart'; +import 'package:airhub_app/widgets/ios_toast.dart'; import 'package:airhub_app/pages/profile/settings_sub_pages.dart'; import 'package:airhub_app/pages/product_selection_page.dart'; 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'; -class SettingsPage extends StatefulWidget { +class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @override - State createState() => _SettingsPageState(); + ConsumerState createState() => _SettingsPageState(); } -class _SettingsPageState extends State { +class _SettingsPageState extends ConsumerState { bool _notificationEnabled = true; @override @@ -70,8 +75,8 @@ class _SettingsPageState extends State { _buildItem( '🔄', '检查更新', - value: '当前最新 1.0.0', - onTap: () => _showMessage('检查更新', '当前已是最新版本 v1.0.0'), + value: '当前 1.0.0', + onTap: _checkUpdate, ), _buildItem( '💻', @@ -289,6 +294,27 @@ class _SettingsPageState extends State { ); } + Future _checkUpdate() async { + try { + final ds = ref.read(systemRemoteDataSourceProvider); + final platform = defaultTargetPlatform == TargetPlatform.iOS ? 'ios' : 'android'; + final result = await ds.checkVersion(platform, '1.0.0'); + if (!mounted) return; + final needUpdate = result['need_update'] as bool? ?? false; + if (needUpdate) { + final latestVersion = result['latest_version'] ?? ''; + final description = result['description'] ?? '有新版本可用'; + _showMessage('发现新版本 v$latestVersion', description as String); + } else { + _showMessage('检查更新', '当前已是最新版本 v1.0.0'); + } + } catch (_) { + if (mounted) { + AppToast.show(context, '检查更新失败,请稍后重试', isError: true); + } + } + } + void _showLogoutDialog() { showGlassDialog( context: context, @@ -297,10 +323,10 @@ class _SettingsPageState extends State { cancelText: '取消', confirmText: '退出', isDanger: true, - onConfirm: () { + onConfirm: () async { Navigator.pop(context); // Close dialog - // In real app: clear session and nav to login - context.go('/login'); + await ref.read(authControllerProvider.notifier).logout(); + if (mounted) context.go('/login'); }, ); } @@ -313,9 +339,16 @@ class _SettingsPageState extends State { cancelText: '取消', confirmText: '确认注销', isDanger: true, - onConfirm: () { + onConfirm: () async { Navigator.pop(context); - _showMessage('已提交', '账号注销申请已提交,将在7个工作日内处理。'); + final success = + await ref.read(authControllerProvider.notifier).deleteAccount(); + if (!mounted) return; + if (success) { + context.go('/login'); + } else { + AppToast.show(context, '注销失败,请稍后重试', isError: true); + } }, ); } diff --git a/airhub_app/lib/pages/settings_page.dart b/airhub_app/lib/pages/settings_page.dart index b881dc5..a1ca3c1 100644 --- a/airhub_app/lib/pages/settings_page.dart +++ b/airhub_app/lib/pages/settings_page.dart @@ -1,27 +1,58 @@ 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'; +import '../features/device/presentation/controllers/device_controller.dart'; -class SettingsPage extends StatefulWidget { +class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @override - State createState() => _SettingsPageState(); + ConsumerState createState() => _SettingsPageState(); } -class _SettingsPageState extends State { - // State for mock data - String _deviceName = '小毛球'; - String _userName = '土豆'; - double _volume = 60; - double _brightness = 85; +class _SettingsPageState extends ConsumerState { + // Local state — initialized from device detail + String _deviceName = ''; + String _userName = ''; + double _volume = 50; + double _brightness = 50; bool _allowInterrupt = true; + bool _privacyMode = false; + int? _userDeviceId; + bool _initialized = false; @override Widget build(BuildContext context) { + // Load current device's detail to populate settings + final devicesAsync = ref.watch(deviceControllerProvider); + final devices = devicesAsync.value ?? []; + if (devices.isNotEmpty) { + _userDeviceId = devices.first.id; + final detailAsync = ref.watch( + deviceDetailControllerProvider(_userDeviceId!), + ); + final detail = detailAsync.value; + if (detail != null && !_initialized) { + _initialized = true; + final s = detail.settings; + if (s != null) { + _deviceName = s.nickname ?? detail.name; + _userName = s.userName ?? ''; + _volume = s.volume.toDouble(); + _brightness = s.brightness.toDouble(); + _allowInterrupt = s.allowInterrupt; + _privacyMode = s.privacyMode; + } else { + _deviceName = detail.name; + } + } + } + return Scaffold( backgroundColor: Colors.transparent, body: Stack( @@ -96,6 +127,7 @@ class _SettingsPageState extends State { '修改设备昵称', _deviceName, (val) => setState(() => _deviceName = val), + settingsKey: 'nickname', ), ), _buildDivider(), @@ -106,6 +138,7 @@ class _SettingsPageState extends State { '修改你的称呼', _userName, (val) => setState(() => _userName = val), + settingsKey: 'user_name', ), ), ]), @@ -117,7 +150,10 @@ class _SettingsPageState extends State { _volume, '🔈', '🔊', - (val) => setState(() => _volume = val), + (val) { + setState(() => _volume = val); + }, + onChangeEnd: (val) => _saveSettings({'volume': val.toInt()}), ), _buildDivider(), _buildSliderItem( @@ -125,7 +161,10 @@ class _SettingsPageState extends State { _brightness, '☀', '☼', - (val) => setState(() => _brightness = val), + (val) { + setState(() => _brightness = val); + }, + onChangeEnd: (val) => _saveSettings({'brightness': val.toInt()}), ), ]), @@ -152,10 +191,20 @@ class _SettingsPageState extends State { _buildToggleItem( '允许打断', _allowInterrupt, - (val) => setState(() => _allowInterrupt = val), + (val) { + setState(() => _allowInterrupt = val); + _saveSettings({'allow_interrupt': val}); + }, ), _buildDivider(), - _buildListTile('隐私模式', '已开启'), + _buildToggleItem( + '隐私模式', + _privacyMode, + (val) { + setState(() => _privacyMode = val); + _saveSettings({'privacy_mode': val}); + }, + ), ]), ], ), @@ -303,8 +352,9 @@ class _SettingsPageState extends State { double value, String iconL, String iconR, - ValueChanged onChanged, - ) { + ValueChanged onChanged, { + ValueChanged? onChangeEnd, + }) { return Padding( padding: const EdgeInsets.all(16), child: Column( @@ -353,6 +403,7 @@ class _SettingsPageState extends State { min: 0, max: 100, onChanged: onChanged, + onChangeEnd: onChangeEnd, ), ), ), @@ -376,11 +427,23 @@ class _SettingsPageState extends State { ); } + Future _saveSettings(Map settings) async { + if (_userDeviceId == null) return; + final controller = ref.read( + deviceDetailControllerProvider(_userDeviceId!).notifier, + ); + final success = await controller.updateSettings(settings); + if (mounted && !success) { + AppToast.show(context, '保存失败', isError: true); + } + } + void _showEditDialog( String title, String initialValue, - ValueSetter onSaved, - ) { + ValueSetter onSaved, { + String? settingsKey, + }) { final controller = TextEditingController(text: initialValue); showGlassDialog( context: context, @@ -394,6 +457,9 @@ class _SettingsPageState extends State { ), onConfirm: () { onSaved(controller.text); + if (settingsKey != null) { + _saveSettings({settingsKey: controller.text}); + } Navigator.pop(context); }, ); @@ -417,15 +483,26 @@ class _SettingsPageState extends State { showGlassDialog( context: context, title: '确认解绑设备?', - description: '解绑后,设备 Airhub_5G 将无法使用。您与 小毛球 的交互数据已形成角色记忆,可注入其他设备。', + description: '解绑后,设备将无法使用。您的交互数据已形成角色记忆,可注入其他设备。', confirmText: '解绑', isDanger: true, - onConfirm: () { - Navigator.pop(context); - Navigator.pop(context); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const ProductSelectionPage()), - ); + onConfirm: () async { + Navigator.pop(context); // close dialog + if (_userDeviceId != null) { + final success = await ref + .read(deviceControllerProvider.notifier) + .unbindDevice(_userDeviceId!); + if (mounted) { + if (success) { + Navigator.pop(context); // close settings + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const ProductSelectionPage()), + ); + } else { + AppToast.show(context, '解绑失败', isError: true); + } + } + } }, ); } diff --git a/airhub_app/lib/pages/wifi_config_page.dart b/airhub_app/lib/pages/wifi_config_page.dart index 8c06de5..c4f4cc7 100644 --- a/airhub_app/lib/pages/wifi_config_page.dart +++ b/airhub_app/lib/pages/wifi_config_page.dart @@ -1,19 +1,24 @@ import 'dart:async'; 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 '../core/services/ble_provisioning_service.dart'; +import '../features/device/presentation/controllers/device_controller.dart'; import '../widgets/animated_gradient_background.dart'; import '../widgets/gradient_button.dart'; -class WifiConfigPage extends StatefulWidget { - const WifiConfigPage({super.key}); +class WifiConfigPage extends ConsumerStatefulWidget { + final Map? extra; + + const WifiConfigPage({super.key, this.extra}); @override - State createState() => _WifiConfigPageState(); + ConsumerState createState() => _WifiConfigPageState(); } -class _WifiConfigPageState extends State +class _WifiConfigPageState extends ConsumerState with TickerProviderStateMixin { int _currentStep = 1; String _selectedWifiSsid = ''; @@ -23,36 +28,112 @@ class _WifiConfigPageState extends State // Progress State double _progress = 0.0; String _progressText = '正在连接WiFi...'; + bool _connectFailed = false; - // Device Info (Mock or from Route Args) - // We'll try to get it from arguments, default to a fallback + // Device Info Map _deviceInfo = {}; - // Mock WiFi List - final List> _wifiList = [ - {'ssid': 'Home_5G', 'level': 4}, - {'ssid': 'Office_WiFi', 'level': 3}, - {'ssid': 'Guest_Network', 'level': 2}, - ]; + // BLE Provisioning + BleProvisioningService? _provService; + List _wifiList = []; + bool _isScanning = false; + + // Subscriptions + StreamSubscription? _wifiListSub; + StreamSubscription? _wifiStatusSub; + StreamSubscription? _disconnectSub; @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Retrieve device info from arguments - final args = ModalRoute.of(context)?.settings.arguments; - if (args is Map) { - _deviceInfo = args; + void initState() { + super.initState(); + _deviceInfo = widget.extra ?? {}; + _provService = _deviceInfo['provService'] as BleProvisioningService?; + + if (_provService != null) { + _setupBleListeners(); + // 自动开始 WiFi 扫描 + _requestWifiScan(); } } - void _handleNext() { + @override + void dispose() { + _wifiListSub?.cancel(); + _wifiStatusSub?.cancel(); + _disconnectSub?.cancel(); + _provService?.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _setupBleListeners() { + // 监听 WiFi 列表 + _wifiListSub = _provService!.onWifiList.listen((list) { + if (!mounted) return; + debugPrint('[WiFi Config] 收到 WiFi 列表: ${list.length} 个'); + setState(() { + _wifiList = list; + _isScanning = false; + }); + }); + + // 监听 WiFi 连接状态 + _wifiStatusSub = _provService!.onWifiStatus.listen((result) { + if (!mounted) return; + debugPrint('[WiFi Config] WiFi 状态: success=${result.success}, reason=${result.reasonCode}'); + if (result.success) { + setState(() { + _progress = 1.0; + _progressText = '配网成功!'; + _currentStep = 4; + }); + } else { + setState(() { + _connectFailed = true; + _progressText = '连接失败 (错误码: ${result.reasonCode})'; + }); + } + }); + + // 监听 BLE 断开 + _disconnectSub = _provService!.onDisconnect.listen((_) { + if (!mounted) return; + debugPrint('[WiFi Config] BLE 连接已断开'); + // 如果在配网中断开,可能是成功后设备重启 + if (_currentStep == 3 && !_connectFailed) { + setState(() { + _progress = 1.0; + _progressText = '设备正在重启...'; + _currentStep = 4; + }); + } + }); + } + + Future _requestWifiScan() async { + if (_provService == null) return; + setState(() => _isScanning = true); + await _provService!.requestWifiScan(); + // WiFi 列表会通过 onWifiList stream 回调 + // 设置超时:10 秒后如果还没收到列表,停止加载 + Future.delayed(const Duration(seconds: 10), () { + if (mounted && _isScanning) { + setState(() => _isScanning = false); + } + }); + } + + Future _handleNext() async { if (_currentStep == 1 && _selectedWifiSsid.isEmpty) return; if (_currentStep == 2 && _passwordController.text.isEmpty) return; if (_currentStep == 4) { - // Navigate to Device Control - // Use pushNamedAndRemoveUntil to remove Bluetooth and WiFi pages from stack - // but keep Home page so back button goes to Home + final sn = _deviceInfo['sn'] as String? ?? ''; + if (sn.isNotEmpty) { + debugPrint('[WiFi Config] Binding device sn=$sn'); + await ref.read(deviceControllerProvider.notifier).bindDevice(sn); + } + if (!mounted) return; context.go('/device-control'); return; } @@ -70,13 +151,58 @@ class _WifiConfigPageState extends State if (_currentStep > 1) { setState(() { _currentStep--; + if (_currentStep == 1) { + _connectFailed = false; + _progress = 0.0; + } }); } else { - context.go('/home'); + _provService?.disconnect(); + context.go('/bluetooth'); } } - void _startConnecting() { + Future _startConnecting() async { + setState(() { + _progress = 0.1; + _progressText = '正在发送WiFi信息...'; + _connectFailed = false; + }); + + if (_provService != null && _provService!.isConnected) { + // 通过 BLE 发送 WiFi 凭证 + setState(() { + _progress = 0.3; + _progressText = '正在发送WiFi凭证...'; + }); + + await _provService!.sendWifiCredentials( + _selectedWifiSsid, + _passwordController.text, + ); + + setState(() { + _progress = 0.5; + _progressText = '等待设备连接WiFi...'; + }); + + // WiFi 状态会通过 onWifiStatus stream 回调 + // 设置超时:60 秒后如果还没收到结果 + Future.delayed(const Duration(seconds: 60), () { + if (mounted && _currentStep == 3 && !_connectFailed) { + setState(() { + _connectFailed = true; + _progressText = '连接超时,请重试'; + }); + } + }); + } else { + // 无 BLE 连接(模拟模式),使用 mock 流程 + _startMockConnecting(); + } + } + + void _startMockConnecting() { const steps = [ {'progress': 0.3, 'text': '正在连接WiFi...'}, {'progress': 0.6, 'text': '正在验证密码...'}, @@ -97,9 +223,7 @@ class _WifiConfigPageState extends State } else { timer.cancel(); if (mounted) { - setState(() { - _currentStep = 4; - }); + setState(() => _currentStep = 4); } } }); @@ -112,34 +236,24 @@ class _WifiConfigPageState extends State resizeToAvoidBottomInset: true, body: Stack( children: [ - // Background - _buildGradientBackground(), - + const AnimatedGradientBackground(), Positioned.fill( child: SafeArea( child: Column( children: [ - // Header _buildHeader(), - - // Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( children: [ - // Steps Indicator _buildStepIndicator(), const SizedBox(height: 32), - - // Dynamic Step Content _buildCurrentStepContent(), ], ), ), ), - - // Footer _buildFooter(), ], ), @@ -150,17 +264,11 @@ class _WifiConfigPageState extends State ); } - // Common Gradient Background - Widget _buildGradientBackground() { - return const AnimatedGradientBackground(); - } - Widget _buildHeader() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( children: [ - // Back button - HTML: bg rgba(255,255,255,0.6), border-radius: 12px, color #4B5563 GestureDetector( onTap: _handleBack, child: Container( @@ -173,7 +281,7 @@ class _WifiConfigPageState extends State child: const Icon( Icons.arrow_back_ios_new, size: 18, - color: Color(0xFF4B5563), // Gray per HTML, not purple + color: Color(0xFF4B5563), ), ), ), @@ -188,7 +296,7 @@ class _WifiConfigPageState extends State ), ), ), - const SizedBox(width: 48), // Balance back button + const SizedBox(width: 48), ], ), ); @@ -209,10 +317,10 @@ class _WifiConfigPageState extends State margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: isCompleted - ? const Color(0xFF22C55E) // Green for completed + ? const Color(0xFF22C55E) : isActive - ? const Color(0xFF8B5CF6) // Purple for active - : const Color(0xFF8B5CF6).withOpacity(0.3), // Faded purple + ? const Color(0xFF8B5CF6) + : const Color(0xFF8B5CF6).withOpacity(0.3), borderRadius: BorderRadius.circular(4), ), ); @@ -235,11 +343,10 @@ class _WifiConfigPageState extends State } } - // Step 1: Select Network + // Step 1: 选择 WiFi 网络 Widget _buildStep1() { return Column( children: [ - // Icon Container( margin: const EdgeInsets.only(bottom: 24), child: const Icon(Icons.wifi, size: 80, color: Color(0xFF8B5CF6)), @@ -255,27 +362,74 @@ class _WifiConfigPageState extends State const SizedBox(height: 8), const Text( '设备需要连接WiFi以使用AI功能', - style: TextStyle( - - fontSize: 14, - color: Color(0xFF6B7280), - ), + style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), const SizedBox(height: 24), - // List - Column( - children: _wifiList.map((wifi) => _buildWifiItem(wifi)).toList(), - ), + if (_isScanning) + const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Column( + children: [ + CircularProgressIndicator(color: Color(0xFF8B5CF6)), + SizedBox(height: 16), + Text( + '正在通过设备扫描WiFi...', + style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), + ), + ], + ), + ) + else if (_wifiList.isEmpty) + Column( + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Text( + '未扫描到WiFi网络', + style: TextStyle(fontSize: 14, color: Color(0xFF9CA3AF)), + ), + ), + GestureDetector( + onTap: _requestWifiScan, + child: const Text( + '重新扫描', + style: TextStyle( + fontSize: 14, + color: Color(0xFF8B5CF6), + decoration: TextDecoration.underline, + ), + ), + ), + ], + ) + else + Column( + children: [ + ..._wifiList.map((wifi) => _buildWifiItem(wifi)), + const SizedBox(height: 8), + GestureDetector( + onTap: _requestWifiScan, + child: const Text( + '重新扫描', + style: TextStyle( + fontSize: 14, + color: Color(0xFF8B5CF6), + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), ], ); } - Widget _buildWifiItem(Map wifi) { - bool isSelected = _selectedWifiSsid == wifi['ssid']; + Widget _buildWifiItem(ScannedWifi wifi) { + bool isSelected = _selectedWifiSsid == wifi.ssid; return GestureDetector( onTap: () { - setState(() => _selectedWifiSsid = wifi['ssid']); + setState(() => _selectedWifiSsid = wifi.ssid); }, child: Container( padding: const EdgeInsets.all(16), @@ -303,27 +457,23 @@ class _WifiConfigPageState extends State children: [ Expanded( child: Text( - wifi['ssid'], + wifi.ssid, style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF1F2937), ), ), ), - // HTML uses per-level SVG icons: wifi-1.svg to wifi-4.svg - Opacity( - opacity: 0.8, - child: SvgPicture.asset( - 'assets/www/icons/wifi-${wifi['level']}.svg', - width: 24, - height: 24, - colorFilter: const ColorFilter.mode( - Color(0xFF6B7280), - BlendMode.srcIn, - ), - ), + // WiFi 信号图标 + Icon( + wifi.level >= 3 + ? Icons.wifi + : wifi.level == 2 + ? Icons.wifi_2_bar + : Icons.wifi_1_bar, + size: 24, + color: const Color(0xFF6B7280), ), ], ), @@ -331,7 +481,7 @@ class _WifiConfigPageState extends State ); } - // Step 2: Enter Password + // Step 2: 输入密码 Widget _buildStep2() { return Column( children: [ @@ -352,16 +502,11 @@ class _WifiConfigPageState extends State ), ), const SizedBox(height: 8), - Text( + const Text( '请输入WiFi密码', - style: TextStyle( - - fontSize: 14, - color: const Color(0xFF6B7280), - ), + style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), const SizedBox(height: 24), - TextField( controller: _passwordController, obscureText: _obscurePassword, @@ -384,9 +529,7 @@ class _WifiConfigPageState extends State size: 22, ), onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); + setState(() => _obscurePassword = !_obscurePassword); }, ), ), @@ -397,11 +540,10 @@ class _WifiConfigPageState extends State ); } - // Step 3: Connecting + // Step 3: 正在连接 Widget _buildStep3() { return Column( children: [ - // Animation placeholder (using Icon for now, can be upgraded to Wave animation) SizedBox( height: 120, child: Center( @@ -415,8 +557,7 @@ class _WifiConfigPageState extends State color: const Color(0xFF8B5CF6).withOpacity(1 - value * 0.5), ); }, - onEnd: - () {}, // Repeat logic usually handled by AnimationController + onEnd: () {}, ), ), ), @@ -429,8 +570,6 @@ class _WifiConfigPageState extends State ), ), const SizedBox(height: 32), - - // Progress Bar ClipRRect( borderRadius: BorderRadius.circular(3), child: SizedBox( @@ -438,7 +577,9 @@ class _WifiConfigPageState extends State child: LinearProgressIndicator( value: _progress, backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.2), - valueColor: const AlwaysStoppedAnimation(Color(0xFF8B5CF6)), + valueColor: AlwaysStoppedAnimation( + _connectFailed ? const Color(0xFFEF4444) : const Color(0xFF8B5CF6), + ), ), ), ), @@ -446,25 +587,42 @@ class _WifiConfigPageState extends State Text( _progressText, style: TextStyle( - fontSize: 14, - color: const Color(0xFF6B7280), + color: _connectFailed ? const Color(0xFFEF4444) : const Color(0xFF6B7280), ), ), + if (_connectFailed) ...[ + const SizedBox(height: 24), + GestureDetector( + onTap: () { + setState(() { + _currentStep = 1; + _connectFailed = false; + _progress = 0.0; + }); + }, + child: const Text( + '返回重新选择', + style: TextStyle( + fontSize: 14, + color: Color(0xFF8B5CF6), + decoration: TextDecoration.underline, + ), + ), + ), + ], ], ); } - // Get device icon path based on device type String _getDeviceIconPath() { final type = _deviceInfo['type'] as String? ?? 'plush'; switch (type) { case 'plush_core': case 'plush': return 'assets/www/icons/pixel-capybara.svg'; - case 'badge_ai': + case 'badgeAi': return 'assets/www/icons/pixel-badge-ai.svg'; - case 'badge_basic': case 'badge': return 'assets/www/icons/pixel-badge-basic.svg'; default: @@ -472,17 +630,15 @@ class _WifiConfigPageState extends State } } - // Step 4: Result (Success) - centered vertically + // Step 4: 配网成功 Widget _buildStep4() { return Column( children: [ const SizedBox(height: 80), - // Success Icon Stack - HTML: no white background Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ - // Device icon container - 120x120 per HTML SizedBox( width: 120, height: 120, @@ -497,7 +653,6 @@ class _WifiConfigPageState extends State ), ), ), - // Check badge Positioned( bottom: -5, right: -5, @@ -531,13 +686,9 @@ class _WifiConfigPageState extends State ), ), const SizedBox(height: 8), - Text( + const Text( '设备已成功连接到网络', - style: TextStyle( - - fontSize: 14, - color: const Color(0xFF6B7280), - ), + style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), ], ); @@ -558,24 +709,24 @@ class _WifiConfigPageState extends State } if (!showNext && _currentStep != 3) { - // Show cancel only? return Padding( padding: const EdgeInsets.all(32), child: TextButton( - onPressed: () => context.go('/bluetooth'), - child: Text( + onPressed: () { + _provService?.disconnect(); + context.go('/bluetooth'); + }, + child: const Text( '取消', - style: TextStyle( - - color: const Color(0xFF6B7280), - ), + style: TextStyle(color: Color(0xFF6B7280)), ), ), ); } - if (_currentStep == 3) - return const SizedBox(height: 100); // Hide buttons during connection + if (_currentStep == 3) { + return const SizedBox(height: 100); + } return Container( padding: EdgeInsets.fromLTRB( @@ -587,7 +738,6 @@ class _WifiConfigPageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Cancel button - frosted glass style if (_currentStep < 4) GestureDetector( onTap: _handleBack, @@ -612,8 +762,6 @@ class _WifiConfigPageState extends State ), ), if (_currentStep < 4) const SizedBox(width: 16), - - // Constrained button (not full-width) GradientButton( text: nextText, onPressed: _handleNext, diff --git a/airhub_app/lib/widgets/feedback_dialog.dart b/airhub_app/lib/widgets/feedback_dialog.dart index 2011123..cddcefd 100644 --- a/airhub_app/lib/widgets/feedback_dialog.dart +++ b/airhub_app/lib/widgets/feedback_dialog.dart @@ -1,11 +1,56 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/theme/design_tokens.dart'; import 'package:airhub_app/widgets/ios_toast.dart'; +import 'package:airhub_app/features/system/data/datasources/system_remote_data_source.dart'; -class FeedbackDialog extends StatelessWidget { +class FeedbackDialog extends ConsumerStatefulWidget { const FeedbackDialog({super.key}); + @override + ConsumerState createState() => _FeedbackDialogState(); +} + +class _FeedbackDialogState extends ConsumerState { + final _contentController = TextEditingController(); + final _contactController = TextEditingController(); + bool _submitting = false; + + @override + void dispose() { + _contentController.dispose(); + _contactController.dispose(); + super.dispose(); + } + + Future _submit() async { + final content = _contentController.text.trim(); + if (content.isEmpty) { + AppToast.show(context, '请输入反馈内容', isError: true); + return; + } + + setState(() => _submitting = true); + + try { + final dataSource = ref.read(systemRemoteDataSourceProvider); + await dataSource.submitFeedback( + content, + contact: _contactController.text.trim(), + ); + if (mounted) { + AppToast.show(context, '感谢您的反馈!'); + Navigator.of(context).pop(); + } + } catch (_) { + if (mounted) { + AppToast.show(context, '提交失败,请稍后重试', isError: true); + setState(() => _submitting = false); + } + } + } + @override Widget build(BuildContext context) { return Dialog( @@ -19,7 +64,7 @@ class FeedbackDialog extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), // Glass effect + color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white.withOpacity(0.5)), ), @@ -38,9 +83,10 @@ class FeedbackDialog extends StatelessWidget { color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(12), ), - child: const TextField( + child: TextField( + controller: _contentController, maxLines: null, - decoration: InputDecoration( + decoration: const InputDecoration( hintText: '请输入您的意见或建议...', border: InputBorder.none, hintStyle: TextStyle( @@ -48,7 +94,27 @@ class FeedbackDialog extends StatelessWidget { fontSize: 14, ), ), - style: TextStyle(fontSize: 14), + style: const TextStyle(fontSize: 14), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + controller: _contactController, + decoration: const InputDecoration( + hintText: '联系方式(选填)', + border: InputBorder.none, + hintStyle: TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + ), + ), + style: const TextStyle(fontSize: 14), ), ), const SizedBox(height: 20), @@ -56,7 +122,7 @@ class FeedbackDialog extends StatelessWidget { children: [ Expanded( child: TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: _submitting ? null : () => Navigator.of(context).pop(), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: const Color(0xFFF3F4F6), @@ -76,10 +142,7 @@ class FeedbackDialog extends StatelessWidget { const SizedBox(width: 12), Expanded( child: TextButton( - onPressed: () { - AppToast.show(context, '感谢您的反馈!'); - Navigator.of(context).pop(); - }, + onPressed: _submitting ? null : _submit, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: const Color(0xFF1F2937), @@ -87,10 +150,19 @@ class FeedbackDialog extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), ), - child: const Text( - '提交', - style: TextStyle(color: Colors.white, fontSize: 16), - ), + child: _submitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '提交', + style: TextStyle(color: Colors.white, fontSize: 16), + ), ), ), ], diff --git a/airhub_app/macos/.gitignore b/airhub_app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/airhub_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/airhub_app/macos/Flutter/Flutter-Debug.xcconfig b/airhub_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/airhub_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/airhub_app/macos/Flutter/Flutter-Release.xcconfig b/airhub_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/airhub_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift b/airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..6b54f39 --- /dev/null +++ b/airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audio_session +import file_selector_macos +import flutter_blue_plus_darwin +import just_audio +import shared_preferences_foundation +import webview_flutter_wkwebview + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) +} diff --git a/airhub_app/macos/Podfile b/airhub_app/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/airhub_app/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/airhub_app/macos/Podfile.lock b/airhub_app/macos/Podfile.lock new file mode 100644 index 0000000..5f0ff20 --- /dev/null +++ b/airhub_app/macos/Podfile.lock @@ -0,0 +1,56 @@ +PODS: + - audio_session (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - FlutterMacOS (1.0.0) + - just_audio (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - FlutterMacOS (from `Flutter/ephemeral`) + - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +EXTERNAL SOURCES: + audio_session: + :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_blue_plus_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin + FlutterMacOS: + :path: Flutter/ephemeral + just_audio: + :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + webview_flutter_wkwebview: + :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin + +SPEC CHECKSUMS: + audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/airhub_app/macos/Runner.xcodeproj/project.pbxproj b/airhub_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7931f62 --- /dev/null +++ b/airhub_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6D999EE5BBA59F7D1818B436 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 215332CEB40E691199867340 /* Pods_Runner.framework */; }; + FB1D261227CA33C7DF9B5A8A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82E99DAA670872DCBD38DA9F /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 215332CEB40E691199867340 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 233001A3A073A38C751D7BBB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* airhub_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = airhub_app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 34A38E1E0F7E112BF790D046 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 6BA93F107E621159A288BC20 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 72CB4B5CDF9C7D67884A99B4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 82E99DAA670872DCBD38DA9F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AAD98D714344307041516A88 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EBC958545224BB7F7AC26750 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FB1D261227CA33C7DF9B5A8A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6D999EE5BBA59F7D1818B436 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 6597D0BE9D7B206A2E3DE1AE /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* airhub_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 6597D0BE9D7B206A2E3DE1AE /* Pods */ = { + isa = PBXGroup; + children = ( + 6BA93F107E621159A288BC20 /* Pods-Runner.debug.xcconfig */, + 72CB4B5CDF9C7D67884A99B4 /* Pods-Runner.release.xcconfig */, + AAD98D714344307041516A88 /* Pods-Runner.profile.xcconfig */, + 34A38E1E0F7E112BF790D046 /* Pods-RunnerTests.debug.xcconfig */, + 233001A3A073A38C751D7BBB /* Pods-RunnerTests.release.xcconfig */, + EBC958545224BB7F7AC26750 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 215332CEB40E691199867340 /* Pods_Runner.framework */, + 82E99DAA670872DCBD38DA9F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 842A7AB26902591B3509C369 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1E8019FA59B44C60956D134B /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 03735CEC23B9AA0EAA7A363F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* airhub_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 03735CEC23B9AA0EAA7A363F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1E8019FA59B44C60956D134B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 842A7AB26902591B3509C369 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 34A38E1E0F7E112BF790D046 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.airlab.airhub.airhubApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/airhub_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/airhub_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 233001A3A073A38C751D7BBB /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.airlab.airhub.airhubApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/airhub_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/airhub_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EBC958545224BB7F7AC26750 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.airlab.airhub.airhubApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/airhub_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/airhub_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/airhub_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/airhub_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/airhub_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/airhub_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/airhub_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..775e644 --- /dev/null +++ b/airhub_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/airhub_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/airhub_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/airhub_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/airhub_app/macos/Runner/AppDelegate.swift b/airhub_app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/airhub_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/airhub_app/macos/Runner/Base.lproj/MainMenu.xib b/airhub_app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/airhub_app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/airhub_app/macos/Runner/Configs/AppInfo.xcconfig b/airhub_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..1ae370c --- /dev/null +++ b/airhub_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = airhub_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.airlab.airhub.airhubApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.airlab.airhub. All rights reserved. diff --git a/airhub_app/macos/Runner/Configs/Debug.xcconfig b/airhub_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/airhub_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/airhub_app/macos/Runner/Configs/Release.xcconfig b/airhub_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/airhub_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/airhub_app/macos/Runner/Configs/Warnings.xcconfig b/airhub_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/airhub_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/airhub_app/macos/Runner/DebugProfile.entitlements b/airhub_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/airhub_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/airhub_app/macos/Runner/Info.plist b/airhub_app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/airhub_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/airhub_app/macos/Runner/MainFlutterWindow.swift b/airhub_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/airhub_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/airhub_app/macos/Runner/Release.entitlements b/airhub_app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/airhub_app/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/airhub_app/macos/RunnerTests/RunnerTests.swift b/airhub_app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/airhub_app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/airhub_app/packages/ali_auth/android/src/main/kotlin/com/sean/rao/ali_auth/AliAuthPlugin.kt b/airhub_app/packages/ali_auth/android/src/main/kotlin/com/sean/rao/ali_auth/AliAuthPlugin.kt new file mode 100644 index 0000000..5db34de --- /dev/null +++ b/airhub_app/packages/ali_auth/android/src/main/kotlin/com/sean/rao/ali_auth/AliAuthPlugin.kt @@ -0,0 +1,22 @@ +package com.sean.rao.ali_auth + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class AliAuthPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(binding.binaryMessenger, "ali_auth") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.notImplemented() + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/airhub_app/packages/ali_auth/ios/Classes/AliAuthPlugin.swift b/airhub_app/packages/ali_auth/ios/Classes/AliAuthPlugin.swift new file mode 100644 index 0000000..fb965bd --- /dev/null +++ b/airhub_app/packages/ali_auth/ios/Classes/AliAuthPlugin.swift @@ -0,0 +1,13 @@ +import Flutter + +public class AliAuthPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "ali_auth", binaryMessenger: registrar.messenger()) + let instance = AliAuthPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result(FlutterMethodNotImplemented) + } +} diff --git a/airhub_app/packages/ali_auth/ios/ali_auth.podspec b/airhub_app/packages/ali_auth/ios/ali_auth.podspec new file mode 100644 index 0000000..537f534 --- /dev/null +++ b/airhub_app/packages/ali_auth/ios/ali_auth.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = 'ali_auth' + s.version = '1.3.7' + s.summary = 'Alibaba Cloud phone auth plugin for Flutter.' + s.homepage = 'https://github.com/CodeGather/flutter_ali_auth' + s.license = { :type => 'MIT' } + s.author = { 'sean' => 'author@example.com' } + s.source = { :http => 'https://github.com/CodeGather/flutter_ali_auth' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + s.swift_version = '5.0' +end diff --git a/airhub_app/packages/ali_auth/lib/ali_auth.dart b/airhub_app/packages/ali_auth/lib/ali_auth.dart new file mode 100644 index 0000000..dc9f534 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'ali_auth_model.dart'; +import 'ali_auth_platform_interface.dart'; + +export 'ali_auth_enum.dart'; +export 'ali_auth_model.dart'; + +/// 阿里云一键登录类 +/// 原来的全屏登录和dialog 统一有配置参数isDislog来控制 +class AliAuth { + /// 初始化监听 + static Stream? onChange({bool type = true}) { + return AliAuthPlatform.instance.onChange(type: type); + } + + /// 获取设备版本信息 + static Future get platformVersion async { + return AliAuthPlatform.instance.getPlatformVersion(); + } + + /// 获取SDK版本号 + static Future get sdkVersion async { + return AliAuthPlatform.instance.getSdkVersion(); + } + + /// 初始化SDK sk 必须 + /// isDialog 是否使用Dialog 弹窗登录 非必须 默认值false 非Dialog登录 + /// debug 是否开启调试模式 非必须 默认true 开启 + /// 使用一键登录传入 SERVICE_TYPE_LOGIN 2 使用号码校验传入 SERVICE_TYPE_AUTH 1 默认值 2 + static Future initSdk(AliAuthModel? config) async { + return AliAuthPlatform.instance.initSdk(config); + } + + /// 一键登录 + static Future login({int timeout = 5000}) async { + return AliAuthPlatform.instance.login(timeout: timeout); + } + + /// 强制关闭一键登录授权页面 + static Future quitPage() async { + return AliAuthPlatform.instance.quitPage(); + } + + /// 强制关闭Loading + static Future hideLoading() async { + return AliAuthPlatform.instance.hideLoading(); + } + + /// 强制关闭一键登录授权页面 + static Future getCurrentCarrierName() async { + return AliAuthPlatform.instance.getCurrentCarrierName(); + } + + /// pageRoute + static Future openPage(String? pageRoute) async { + return AliAuthPlatform.instance.openPage(pageRoute); + } + + static Future get checkCellularDataEnable async { + return AliAuthPlatform.instance.checkCellularDataEnable; + } + + /// 苹果登录iOS专用 + static Future get appleLogin async { + return AliAuthPlatform.instance.appleLogin; + } + + /// 数据监听 + static loginListen( + {bool type = true, + required Function onEvent, + Function? onError, + isOnlyOne = true}) async { + return AliAuthPlatform.instance.loginListen( + type: type, onEvent: onEvent, onError: onError, isOnlyOne: isOnlyOne); + } + + /// 暂停 + static pause() { + return AliAuthPlatform.instance.pause(); + } + + /// 恢复 + static resume() { + return AliAuthPlatform.instance.resume(); + } + + /// 销毁监听 + static dispose() { + return AliAuthPlatform.instance.dispose(); + } + + /// WEB专用接口 + static Future checkAuthAvailable(String accessToken, String jwtToken, + {required Function(dynamic) success, + required Function(dynamic) error}) async { + await AliAuthPlatform.instance + .checkAuthAvailable(accessToken, jwtToken, success, error); + } + + /// WEB专用接口 + static Future getVerifyToken( + {required Function(dynamic) success, + required Function(dynamic) error}) async { + await AliAuthPlatform.instance.getVerifyToken(success, error); + } +} diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_enum.dart b/airhub_app/packages/ali_auth/lib/ali_auth_enum.dart new file mode 100644 index 0000000..98fe3c2 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_enum.dart @@ -0,0 +1,242 @@ +import 'dart:io'; + +/// 本机号码校验,一键登录 +enum SdkType { auth, login } + +/// ScaleType 可选类型 +enum ScaleType { + matrix, + fitXy, + fitStart, + fitCenter, + fitEnd, + center, + centerCrop, + centerInside, +} + +enum ContentMode { + scaleToFill, + scaleAspectFit, // contents scaled to fit with fixed aspect. remainder is transparent + scaleAspectFill, // contents scaled to fill with fixed aspect. some portion of content may be clipped. + redraw, // redraw on bounds change (calls -setNeedsDisplay) + center, // contents remain same size. positioned adjusted. + top, + bottom, + left, + right, + topLeft, + topRight, + bottomLeft, + bottomRight, +} + +enum Gravity { centerHorizntal, left, right } + +enum UIFAG { + systemUiFalgLowProfile, + systemUiFalgHideNavigation, + systemUiFalgFullscreen, + systemUiFalgLayoutStable, + systemUiFalgLayoutHideNavigtion, + systemUiFalgLayoutFullscreen, + systemUiFalgImmersive, + systemUiFalgImmersiveSticky, + systemUiFalgLightStatusBar, + systemUiFalgLightNavigationBar +} + +enum PNSPresentationDirection { + presentationDirectionBottom, + presentationDirectionRight, + presentationDirectionTop, + presentationDirectionLeft, +} + +enum PageType { + ///全屏(竖屏) + fullPort, + + ///全屏(横屏) + fullLand, + + ///弹窗(竖屏) + dialogPort, + + ///"弹窗(横屏) + dialogLand, + + ///底部弹窗 + dialogBottom, + + ///自定义View + customView, + + ///自定义View(Xml) + customXml, + + /// 自定义背景GIF + customGif, + + /// 自定义背景视频 + customMOV, + + /// 自定义背景图片 + customPIC, +} + +class EnumUtils { + static int formatGravityValue(Gravity? status) { + switch (status) { + case Gravity.centerHorizntal: + return 1; + case Gravity.left: + if (Platform.isAndroid) { + return 3; + } else { + return 0; + } + case Gravity.right: + if (Platform.isAndroid) { + return 5; + } else { + return 2; + } + default: + return 4; + } + } + + static int formatUiFagValue(UIFAG? status) { + switch (status) { + case UIFAG.systemUiFalgLowProfile: + return 1; + case UIFAG.systemUiFalgHideNavigation: + return 2; + case UIFAG.systemUiFalgFullscreen: + return 4; + case UIFAG.systemUiFalgLayoutStable: + return 256; + case UIFAG.systemUiFalgLayoutHideNavigtion: + return 512; + case UIFAG.systemUiFalgLayoutFullscreen: + return 1024; + case UIFAG.systemUiFalgImmersive: + return 2048; + case UIFAG.systemUiFalgImmersiveSticky: + return 4096; + case UIFAG.systemUiFalgLightStatusBar: + return 8192; + default: + return 16; + } + } +} + +/// 第三方布局实体 +class CustomThirdView { + late int? top; + late int? right; + late int? bottom; + late int? left; + late int? width; + late int? height; + late int? space; + late int? size; + late String? color; + late int? itemWidth; + late int? itemHeight; + late List? viewItemName; + late List? viewItemPath; + CustomThirdView( + this.top, + this.right, + this.bottom, + this.left, + this.width, + this.height, + this.space, + this.size, + this.color, + this.itemWidth, + this.itemHeight, + this.viewItemName, + this.viewItemPath); + + factory CustomThirdView.fromJson(Map srcJson) => + _$CustomThirdViewFromJson(srcJson); + Map toJson() => _$CustomThirdViewToJson(this); +} + +/// 第三方布局json转实体 +CustomThirdView _$CustomThirdViewFromJson(Map json) { + return CustomThirdView( + json['top'], + json['right'], + json['bottom'], + json['left'], + json['width'], + json['height'], + json['space'], + json['size'], + json['color'], + json['itemWidth'], + json['itemHeight'], + json['viewItemName'], + json['viewItemPath']); +} + +/// 第三方布局实体转json +Map _$CustomThirdViewToJson(CustomThirdView instance) => + { + 'top': instance.top, + 'right': instance.right, + 'bottom': instance.bottom, + 'left': instance.left, + 'width': instance.width, + 'height': instance.height, + 'space': instance.space, + 'size': instance.size, + 'color': instance.color, + 'itemWidth': instance.itemWidth, + 'itemHeight': instance.itemHeight, + 'viewItemName': instance.viewItemName, + 'viewItemPath': instance.viewItemPath, + }; + +/// 自定义布局实体 +class CustomView { + late int? top; + late int? right; + late int? bottom; + late int? left; + late int? width; + late int? height; + late String? imgPath; + late ScaleType? imgScaleType; + CustomView(this.top, this.right, this.bottom, this.left, this.width, + this.height, this.imgPath, this.imgScaleType); + + factory CustomView.fromJson(Map srcJson) => + _$CustomViewFromJson(srcJson); + Map toJson() => _$CustomViewToJson(this); +} + +/// 自定义布局json转实体 +CustomView _$CustomViewFromJson(Map json) { + return CustomView(json['top'], json['right'], json['bottom'], json['left'], + json['width'], json['height'], json['imgPath'], json['imgScaleType']); +} + +/// 自定义布局实体转json +Map _$CustomViewToJson(CustomView instance) => + { + 'top': instance.top ?? 0, + 'right': instance.right ?? 0, + 'bottom': instance.bottom ?? 0, + 'left': instance.left ?? 0, + 'width': instance.width, + 'height': instance.height, + 'imgPath': instance.imgPath, + 'imgScaleType': (instance.imgScaleType ?? ScaleType.centerCrop).index, + }; diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_method_channel.dart b/airhub_app/packages/ali_auth/lib/ali_auth_method_channel.dart new file mode 100644 index 0000000..7fa39f6 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_method_channel.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'ali_auth_model.dart'; +import 'ali_auth_platform_interface.dart'; + +/// 阿里云一键登录类 +/// 原来的全屏登录和dialog 统一有配置参数isDislog来控制 +class MethodChannelAliAuth extends AliAuthPlatform { + /// 声明回调通道 + @visibleForTesting + final methodChannel = const MethodChannel('ali_auth'); + + /// 声明监听回调通道 + @visibleForTesting + final EventChannel eventChannel = const EventChannel("ali_auth/event"); + + /// 监听器 + static Stream? onListener; + + /// 为了控制Stream 暂停。恢复。取消监听 新建 + static StreamSubscription? streamSubscription; + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + @override + Future getSdkVersion() async { + final version = await methodChannel.invokeMethod('getSdkVersion'); + return version; + } + + /// 初始化监听 + @override + Stream? onChange({bool type = true}) { + onListener ??= eventChannel.receiveBroadcastStream(type); + return onListener; + } + + /// 初始化SDK sk 必须 + /// isDialog 是否使用Dialog 弹窗登录 非必须 默认值false 非Dialog登录 + /// debug 是否开启调试模式 非必须 默认true 开启 + /// 使用一键登录传入 SERVICE_TYPE_LOGIN 2 使用号码校验传入 SERVICE_TYPE_AUTH 1 默认值 2 + @override + Future initSdk(AliAuthModel? config) async { + config ??= AliAuthModel("", ""); + return await methodChannel.invokeMethod("initSdk", config.toJson()); + } + + /// 一键登录 + @override + Future login({int timeout = 5000}) async { + return await methodChannel.invokeMethod('login', {"timeout": timeout}); + } + + /// 强制关闭一键登录授权页面 + @override + Future quitPage() async { + return await methodChannel.invokeMethod('quitPage'); + } + + /// SDK环境检查函数,检查终端是否支持号码认证。 + /// + /// @see PhoneNumberAuthHelper#SERVICE_TYPE_AUTH 本机号码校验 + /// @see PhoneNumberAuthHelper#SERVICE_TYPE_LOGIN 一键登录校验 + @override + Future checkEnvAvailable() async { + return await methodChannel.invokeMethod('checkEnvAvailable'); + } + + /// 获取授权页协议勾选框选中状态 + @override + Future queryCheckBoxIsChecked() async { + return await methodChannel.invokeMethod('queryCheckBoxIsChecked'); + } + + /// 获取授权页协议勾选框选中状态 + @override + Future setCheckboxIsChecked() async { + return await methodChannel.invokeMethod('setCheckboxIsChecked'); + } + + /// 强制关闭Loading + @override + Future hideLoading() async { + return await methodChannel.invokeMethod('hideLoading'); + } + + /// 强制关闭一键登录授权页面 + @override + Future getCurrentCarrierName() async { + return await methodChannel.invokeMethod('getCurrentCarrierName'); + } + + /// pageRoute + @override + Future openPage(String? pageRoute) async { + return await methodChannel + .invokeMethod('openPage', {'pageRoute': pageRoute ?? 'main_page'}); + } + + @override + Future get checkCellularDataEnable async { + return await methodChannel.invokeMethod('checkCellularDataEnable'); + } + + /// 苹果登录iOS专用 + @override + Future get appleLogin async { + return await methodChannel.invokeMethod('appleLogin'); + } + + /// 数据监听 + @override + loginListen( + {bool type = true, + required Function onEvent, + Function? onError, + isOnlyOne = true}) async { + /// 默认为初始化单监听 + if (isOnlyOne && streamSubscription != null) { + /// 原来监听被移除 + dispose(); + } + streamSubscription = onChange(type: type)!.listen( + onEvent as void Function(dynamic)?, + onError: onError, + onDone: null, + cancelOnError: null); + } + + /// 暂停 + @override + pause() { + if (streamSubscription != null) { + streamSubscription!.pause(); + } + } + + /// 恢复 + @override + resume() { + if (streamSubscription != null) { + streamSubscription!.resume(); + } + } + + /// 销毁监听 + @override + dispose() { + if (streamSubscription != null) { + streamSubscription!.cancel(); + streamSubscription = null; + } + } +} diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_model.dart b/airhub_app/packages/ali_auth/lib/ali_auth_model.dart new file mode 100644 index 0000000..4b030c8 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_model.dart @@ -0,0 +1,1114 @@ +import 'ali_auth_enum.dart'; + +/// 登录窗口配置 +class AliAuthModel { + /// aliyun sk + late String? androidSk; + + /// aliyun sk + late String? iosSk; + + /// 是否开启debug模式 + late bool? isDebug; + + /// 是否延迟 + late bool? isDelay; + + /// 页面类型 必须 + late PageType? pageType; + + // /// 8. ⻚⾯相关函数 + // + // /// 设置授权⻚进场动画 + // late String? authPageActIn; + // + // /// 设置授权⻚退出动画 + // late String? authPageActOut; + // + // /// 设置授权⻚背景图drawable资源的⽬录,不需要加后缀,⽐如图⽚在drawable中的存放⽬录是res/drawablexxhdpi/loading.png,则传⼊参数为"loading",setPageBackgroundPath("loading")。 + // late String? pageBackgroundPath; + // + // /// dialog 蒙层的透明度 + // late double? dialogAlpha; + // + // /// 设置弹窗模式授权⻚宽度,单位dp,设置⼤于0即为弹窗模式 + // late int? dialogWidth; + // + // /// 设置弹窗模式授权⻚⾼度,单位dp,设置⼤于0即为弹窗模式 + // late int? dialogHeight; + // + // /// 设置弹窗模式授权⻚X轴偏移,单位dp + // late int? dialogOffsetX; + // + // /// 设置弹窗模式授权⻚Y轴偏移,单位dp + // late int? dialogOffsetY; + // + // /// 设置授权⻚是否居于底部 + // late bool? dialogBottom; + // + /// ------- 一、状态栏 --------- /// + + /// statusBarColor 设置状态栏颜⾊(系统版本 5.0 以上可设置) + late String? statusBarColor; + + /// 设置状态栏文字颜色(系统版本6.0以上可设置黑色白色),true为黑色 + late bool? lightColor; + + /// 设置状态栏是否隐藏 + late bool? isStatusBarHidden; + + /// 设置状态栏U属性 + late UIFAG? statusBarUIFlag; + + /// 设置协议⻚状态栏颜⾊(系统版本 5.0 以上可设置)不设置则与授权⻚设置⼀致 + late String? webViewStatusBarColor; + + /// ------- 二、导航栏 --------- /// + + /// 设置默认导航栏是否隐藏 + late bool? navHidden; + + /// 设置导航栏主题色 + late String? navColor; + + /// 设置导航栏标题文案内容 + late String? navText; + + /// 设置导航栏标题文字颜色 + late String? navTextColor; + + /// 设置导航栏标题文字大小 + /// @Deprecated("即将删除的属性......") + late int? navTextSize; + + /// 设置导航栏返回按钮图片路径 + late String? navReturnImgPath; + + /// 设置导航栏返回按钮隐藏 + late bool? navReturnHidden; + + /// 设置导航栏返回按钮宽度 + late int? navReturnImgWidth; + + /// 设置导航栏返回按钮隐藏高度 + late int? navReturnImgHeight; + + /// 自定义返回按钮参数 + late CustomView? customReturnBtn; + + /// 设置导航栏返回按钮缩放模式 + late ScaleType? navReturnScaleType; + + /// 设置协议页顶部导航栏背景色不设置则与授权页设置一致 + late String? webNavColor; + + /// 设置协议页顶部导航栏标题颜色不设置则与授权页设置一致 + late String? webNavTextColor; + + /// 设置协议页顶部导航栏文字大小不设置则与授权页设置一 + late int? webNavTextSize; + + /// 设置协议页导航栏返回按钮图片路径不设置则与授权页设 + late String? webNavReturnImgPath; + + /// ------- 三、LOGO区 --------- /// + + /// 设置logo 图⽚ + late String? logoImgPath; + + /// 隐藏logo + late bool? logoHidden; + + /// 设置logo 控件宽度 + late int? logoWidth; + + /// 设置logo 控件⾼度 + late int? logoHeight; + + /// 设置logo 控件相对导航栏顶部的位移,单位dp + late int? logoOffsetY; + + /// 设置logo 控件相对底部的位移,单位dp + // ignore: non_constant_identifier_names + late int? logoOffsetY_B; + + /// 设置logo图⽚缩放模式 + /// FIT_XY, + /// FIT_START, + /// FIT_CENTER, + /// FIT_END, + /// CENTER, + /// CENTER_CROP, + /// CENTER_INSIDE + late ScaleType? logoScaleType; + + /// ------- 四、slogan区 --------- /// + + /// 设置是否隐藏slogan + late bool? sloganHidden; + + /// 设置slogan ⽂字内容 + late String? sloganText; + + /// 设置slogan ⽂字颜⾊ + late String? sloganTextColor; + + /// 设置slogan ⽂字⼤⼩ + /// @Deprecated("即将删除的属性......") + late int? sloganTextSize; + + /// 设置slogan 相对导航栏顶部的 位移,单位dp + late int? sloganOffsetY; + + /// 设置slogan 相对底部的 位移,单位dp + // ignore: non_constant_identifier_names + late int? sloganOffsetY_B; + + /// ------- 五、掩码栏 --------- /// + + /// 设置⼿机号码字体颜⾊ + late String? numberColor; + + /// 设置⼿机号码字体⼤⼩ + /// @Deprecated("即将删除的属性......") + late int? numberSize; + + /// 设置号码栏控件相对导航栏顶部的位移,单位 dp + late int? numFieldOffsetY; + + /// 设置号码栏控件相对底部的位移,单位 dp + // ignore: non_constant_identifier_names + late int? numFieldOffsetY_B; + + /// 设置号码栏相对于默认位置的X轴偏移量,单位dp + late int? numberFieldOffsetX; + + /// 设置⼿机号掩码的布局对⻬⽅式,只⽀持 + /// Gravity.CENTER_HORIZONTAL、 + /// Gravity.LEFT、 + /// Gravity.RIGHT三种对⻬⽅式 + late Gravity? numberLayoutGravity; + + /// ------- 六、登录按钮 --------- /// + + /// 设置登录按钮⽂字 + late String? logBtnText; + + /// 设置登录按钮⽂字颜⾊ + late String? logBtnTextColor; + + /// 设置登录按钮⽂字⼤⼩ + /// @Deprecated("即将删除的属性......") + late int? logBtnTextSize; + + /// 设置登录按钮宽度,单位 dp + late int? logBtnWidth; + + /// 设置登录按钮⾼度,单位dp + late int? logBtnHeight; + + /// 设置登录按钮相对于屏幕左右边缘边距 + late int? logBtnMarginLeftAndRight; + + /// login_btn_bg_xml + /// 设置登录按钮背景图⽚路径 是一个逗号拼接的图片路径 例如:'assets/login_btn_normal.png,assets/login_btn_unable.png,assets/login_btn_press.png' + /// 如果设置错误或者找不到图片则使用默认样式 + late String? logBtnBackgroundPath; + + /// 设置登录按钮相对导航栏顶部的位移,单位 dp + late int? logBtnOffsetY; + + /// 设置登录按钮相对底部的位移,单位 dp + // ignore: non_constant_identifier_names + late int? logBtnOffsetY_B; + + /// 设置登录loading dialog 背景图⽚路径24 + late String? loadingImgPath; + + /// 设置登陆按钮X轴偏移量,如果设置了setLogBtnMarginLeftAndRight,并且布局对⻬⽅式为左对⻬或者右对⻬,则会在margin的基础上再增加offsetX的偏移量,如果是居中对⻬,则仅仅会在居中的基础上再做offsetX的偏移。 + late int? logBtnOffsetX; + + /// 设置登陆按钮布局对⻬⽅式, + /// 只⽀持Gravity.CENTER_HORIZONTAL、 + /// Gravity.LEFT、 + /// Gravity.RIGHT三种对⻬⽅式 + late Gravity? logBtnLayoutGravity; + + /// ------- 七、切换到其他方式 --------- /// + + /// 设置切换按钮点是否可⻅ + late bool? switchAccHidden; + + /// 是否需要点击切换按钮时校验是否勾选协议 默认值true + late bool? switchCheck; + + /// 设置切换按钮⽂字内容 + late String? switchAccText; + + /// 设置切换按钮⽂字颜⾊ + late String? switchAccTextColor; + + /// 设置切换按钮⽂字⼤⼩ + /// @Deprecated("即将删除的属性......") + late int? switchAccTextSize; + + /// 设置换按钮相对导航栏顶部的位移,单位 dp + late int? switchOffsetY; + + /// 设置换按钮相对底部的位移,单位 dp + // ignore: non_constant_identifier_names + late int? switchOffsetY_B; + + /// ------- 八、自定义控件区 --------- /// + + /// 是否隐藏第三方布局 + late bool? isHiddenCustom; + + // late bool? isCheckboxCustomViewClick; + + /// 第三方图标相关参数只对iOS有效,android 请使用布局文件实现 + /// 第三方图标按钮居中布局 + /// 第三方布局图片路径 + late CustomThirdView? customThirdView; + + /// ------- 九、协议栏 --------- /// + + /// 自定义第一条名称 + late String? protocolOneName; + + /// 自定义第一条url + late String? protocolOneURL; + + /// 设置授权页运营商协议文本颜色。 + late String? protocolOwnColor; + + /// 设置授权页协议1文本颜色。 + late String? protocolOwnOneColor; + + /// 授权页协议2文本颜色。 + late String? protocolOwnTwoColor; + + /// 授权页协议3文本颜色。 + late String? protocolOwnThreeColor; + + /// 自定义第二条名称 + late String? protocolTwoName; + + /// 自定义第二条url + late String? protocolTwoURL; + + /// 自定义第三条名称 + late String? protocolThreeName; + + /// 自定义第三条url + late String? protocolThreeURL; + + /// 自定义协议名称颜色 + late String? protocolCustomColor; + + /// 基础文字颜色 + late String? protocolColor; + + /// ------- 十、其它全屏属性 --------- /// + + /// 设置隐私条款相对导航栏顶部的位移,单位dp + late int? privacyOffsetY; + + /// 设置隐私条款相对底部的位移,单位dp + // ignore: non_constant_identifier_names + late int? privacyOffsetY_B; + + /// 设置隐私条款是否默认勾选 + late bool? privacyState; + + /// 设置隐私条款文字对齐方式,单位Gravity.xx + late Gravity? protocolLayoutGravity; + + /// 设置隐私条款文字大小 + late int? privacyTextSize; + + /// 设置隐私条款距离手机左右边缘的边距,单位dp + late int? privacyMargin; + + /// 设置开发者隐私条款前置自定义文案 + late String? privacyBefore; + + /// 设置开发者隐私条款尾部自定义文案 + late String? privacyEnd; + + /// 设置复选框是否隐藏 + late bool? checkboxHidden; + + /// 设置复选框未选中时图片 + late String? uncheckedImgPath; + + /// 设置复选框未选中时图片 + late String? checkedImgPath; + + /// 复选框图片的宽度 + late int? checkBoxWidth; + + /// 复选框图片的高度 + late int? checkBoxHeight; + + /// 设置隐私栏的布局对齐方式,该接口控制了整个隐私栏 + late Gravity? protocolGravity; + + /// 设置隐私条款X的位移,单位dp + late int? privacyOffsetX; + + /// 设置运营商协议后缀符号,只能设置⼀个字符,且只能设置<>()《》【】『』[]()中的⼀个 + late String? vendorPrivacyPrefix; + + /// 设置运营商协议后缀符号,只能设置⼀个字符,且只能设置<>()《》【】『』[]()中的⼀个 + late String? vendorPrivacySuffix; + + /// 设置checkbox未勾选时,点击登录按钮toast是否隐藏 (android 独有) + late bool? logBtnToastHidden; + + /// 设置底部虚拟按键背景⾊(系统版本 5.0 以上可设置) + late String? bottomNavColor; + + /// 授权页弹窗模式点击非弹窗区域关闭授权页 + late bool? tapAuthPageMaskClosePage; + + /// 弹窗宽度 + late int? dialogWidth; + + /// 弹窗高度 + late int? dialogHeight; + + /// 是否是底部弹窗默认false + late bool? dialogBottom; + late int? dialogOffsetX; + late int? dialogOffsetY; + late List? dialogCornerRadiusArray; + late double? dialogAlpha; + + /// 背景图片圆角 + /// dialog安卓端有效 iOS无效 + late int? pageBackgroundRadius; + late bool? webSupportedJavascript; + + /// setAuthPageActIn + late String? authPageActIn; + late String? activityOut; + + /// setAuthPageActOut + late String? authPageActOut; + late String? activityIn; + + late int? screenOrientation; + late List? privacyConectTexts; + late int? privacyOperatorIndex; + + /// 暴露名 + late String? protocolAction; + + /// 包名 + late String? packageName; + + late String? loadingBackgroundPath; + + /// 是否隐藏loading + late bool? isHiddenToast; + + /// 是否隐藏loading + late bool? autoHideLoginLoading; + + /// 底部虚拟导航栏 + late String? bottomNavBarColor; + + /// 授权页面背景色 + late String? backgroundColor; + + /// 授权页面背景路径支持视频mp4,mov等、图片jpeg,jpg,png等、动图gif + late String? pageBackgroundPath; + + /// + late ContentMode? backgroundImageContentMode; + + /// /// ------- 十一、ios 弹窗设置参数 --------- /// + /// 是否隐藏bar bar 为true 时 alertCloseItemIsHidden 也为true + late bool? alertBarIsHidden; + + /// bar的背景色 默认颜色为白色 #FFFFFF + late String? alertTitleBarColor; + + /// bar的关闭按钮 + late bool? alertCloseItemIsHidden; + + /// 关闭按钮的图片路径 + late String? alertCloseImagePath; + + /// 关闭按钮的图片X坐标 + late int? alertCloseImageX; + + /// 关闭按钮的图片Y坐标 + late int? alertCloseImageY; + + /// 关闭按钮的图片宽度 + late int? alertCloseImageW; + + /// 关闭按钮的图片高度 + late int? alertCloseImageH; + + /// 底部蒙层背景颜色,默认黑色 + late String? alertBlurViewColor; + + /// 底部蒙层背景透明度,默认0.5 0 ~ 1 + late double? alertBlurViewAlpha; + + late PNSPresentationDirection? presentDirection; + + /// /// ------- 十二、二次弹窗设置 --------- /// + /// 设置二次隐私协议弹窗是否需要显示。false(默认值) + late bool? privacyAlertIsNeedShow; + + /// 设置二次隐私协议弹窗点击按钮是否需要执行登录 true(默认值) + late bool? privacyAlertIsNeedAutoLogin; + + /// 设置二次隐私协议弹窗显示自定义动画。 + late String? privacyAlertEntryAnimation; + + /// 设置二次隐私协议弹窗隐藏自定义动画。 + late String? privacyAlertExitAnimation; + + /// 设置二次隐私协议弹窗的四个圆角值。说明 顺序为左上、右上、右下、左下,需要填充4个值,不足4个值则无效,如果值小于等于0则为直角。 + late List? privacyAlertCornerRadiusArray; + + /// 设置二次隐私协议弹窗背景色(同意并继续按钮区域)。 + late String? privacyAlertBackgroundColor; + + /// 设置二次隐私协议弹窗透明度。默认值1.0。 + late double privacyAlertAlpha; + + /// 二次隐私协议弹窗标题文字内容默认"请阅读并同意以下条款" + late String? privacyAlertTitleContent; + + /// 设置标题文字大小,默认值18 sp。 + late int? privacyAlertTitleTextSize; + + /// 设置标题文字颜色。 + late String? privacyAlertTitleColor; + + /// 设置二次隐私协议弹窗标题背景颜色。 + late String? privacyAlertTitleBackgroundColor; + + /// 设置二次隐私协议弹窗标题支持居中、居左,默认居中显示。 + late Gravity? privacyAlertTitleAlignment; + + /// 设置服务协议文字大小,默认值16 sp。 + late int? privacyAlertContentTextSize; + + /// 设置协议内容背景颜色。 + late String? privacyAlertContentBackgroundColor; + + /// 设置二次隐私协议弹窗背景蒙层是否显示。true(默认值) + late bool privacyAlertMaskIsNeedShow; + + /// 设置二次隐私协议弹窗蒙层透明度。默认值0.3 + late double privacyAlertMaskAlpha; + + /// 蒙层颜色。 + late String? privacyAlertMaskColor; + + /// 设置屏幕居中、居上、居下、居左、居右,默认居中显示。 + late Gravity? privacyAlertAlignment; + + /// 设置弹窗宽度。 + late int? privacyAlertWidth; + + /// 设置弹窗高度。 + late int? privacyAlertHeight; + + /// 设置弹窗水平偏移量。(单位:dp) + late int? privacyAlertOffsetX; + + /// 设置弹窗竖直偏移量。(单位:dp) + late int? privacyAlertOffsetY; + + /// 设置标题文字水平偏移量。(单位:dp) + late int? privacyAlertTitleOffsetX; + + /// 设置标题文字竖直偏移量。(单位:dp) + late int? privacyAlertTitleOffsetY; + + /// 设置二次隐私协议弹窗协议文案支持居中、居左,默认居左显示。 + late Gravity? privacyAlertContentAlignment; + + /// 设置服务协议文字颜色。 + late String? privacyAlertContentColor; + + /// 设置授权页协议1文本颜色。 + late String? privacyAlertOwnOneColor; + + /// 设置授权页协议2文本颜色。 + late String? privacyAlertOwnTwoColor; + + /// 设置授权页协议3文本颜色。 + late String? privacyAlertOwnThreeColor; + + /// 设置授权页运营商协议文本颜色。 + late String? privacyAlertOperatorColor; + + /// 设置服务协议非协议文字颜色。 + late String? privacyAlertContentBaseColor; + + /// 二次弹窗协议名称是否添加下划线, 默认false + late bool? privacyAlertProtocolNameUseUnderLine; + + /// 设置服务协议左右两侧间距。 + late int? privacyAlertContentHorizontalMargin; + + /// 设置服务协议上下间距。 + late int? privacyAlertContentVerticalMargin; + + /// 设置按钮背景图片路径。 + late String? privacyAlertBtnBackgroundImgPath; + + /// 二次弹窗协议前缀。 + late String? privacyAlertBefore; + + /// 二次弹窗协议后缀。 + late String? privacyAlertEnd; + + /// 设置确认按钮文本。 + late String? privacyAlertBtnText; + + /// 设置按钮文字颜色。 + late String? privacyAlertBtnTextColor; + + /// 设置按钮文字大小,默认值18 sp。 + late int? privacyAlertBtnTextSize; + + /// 设置按钮宽度。(单位:dp) + late int? privacyAlertBtnWidth; + + /// 设置按钮高度。(单位:dp) + late int? privacyAlertBtnHeigth; + + /// 设置右上角的关闭按钮。true(默认值):显示关闭按钮。 + late bool? privacyAlertCloseBtnShow; + + /// 关闭按钮图片路径。 + late String? privacyAlertCloseImagPath; + + /// 关闭按钮缩放类型。 + late ScaleType? privacyAlertCloseScaleType; + + /// 关闭按钮宽度。(单位:dp) + late int? privacyAlertCloseImgWidth; + + /// 关闭按钮高度。(单位:dp) + late int? privacyAlertCloseImgHeight; + + /// 设置二次隐私协议弹窗点击背景蒙层是否关闭弹窗。true(默认值):表示关闭 + late bool tapPrivacyAlertMaskCloseAlert; + + /// 成功获取token后是否自动关闭授权页面 + late bool? autoQuitPage; + + /// /// ------- 十三、toast设置 --------- /// + /// 为勾选用户协议时的提示文字 + late bool? isHideToast; + + /// 为勾选用户协议时的提示文字 + late String? toastText; + + /// toast的背景色 + late String? toastBackground; + + /// 文字颜色 + late String? toastColor; + + /// toast的padding + late int? toastPadding; + + /// 只有设置mode为top时才起作用,距离顶部的距离 + late int? toastMarginTop; + + /// 只有设置mode为bottom时才起作用,距离低部的距离 + late int? toastMarginBottom; + + /// toast的显示位置可用值 top、center、bottom + late String? toastPositionMode; + + /// 关闭的时长 默认3s + late int? toastDelay; + + /// 横屏水滴屏全屏适配 默认false + late bool? fullScreen; + + /// 授权页是否跟随系统深色模式 默认false + late bool? authPageUseDayLight; + + /// SDK内置所有界面隐藏底部导航栏 默认false + late bool? keepAllPageHideNavigationBar; + + /// 授权页物理返回键禁用 默认false + late bool? closeAuthPageReturnBack; + + AliAuthModel( + this.androidSk, + this.iosSk, { + this.isDebug = true, + this.isDelay = false, + this.pageType = PageType.fullPort, + this.privacyOffsetX, + this.statusBarColor, + this.bottomNavColor, + this.lightColor, + this.isStatusBarHidden, + this.statusBarUIFlag, + this.navColor, + this.navText, + this.navTextColor, + this.navReturnImgPath, + this.navReturnImgWidth, + this.navReturnImgHeight, + this.customReturnBtn, + this.navReturnHidden, + this.navReturnScaleType, + this.navHidden, + this.logoImgPath, + this.logoHidden, + this.numberColor, + this.numberSize, + this.switchAccHidden, + this.switchCheck, + this.switchAccTextColor, + this.logBtnText, + this.logBtnTextSize, + this.logBtnTextColor, + this.protocolOneName, + this.protocolOneURL, + this.protocolTwoName, + this.protocolTwoURL, + this.protocolThreeName, + this.protocolThreeURL, + this.protocolCustomColor, + this.protocolColor, + this.protocolLayoutGravity, + this.sloganTextColor, + + /// 授权页运营商协议文本颜色。 + this.protocolOwnColor, + + /// 授权页协议1文本颜色。 + this.protocolOwnOneColor, + + /// 授权页协议2文本颜色。 + this.protocolOwnTwoColor, + + /// 授权页协议3文本颜色。 + this.protocolOwnThreeColor, + this.sloganText, + this.logBtnBackgroundPath, + this.loadingImgPath, + this.sloganOffsetY, + this.logoOffsetY, + // ignore: non_constant_identifier_names + this.logoOffsetY_B, + this.logoScaleType, + this.numFieldOffsetY, + // ignore: non_constant_identifier_names + this.numFieldOffsetY_B, + this.numberFieldOffsetX, + this.numberLayoutGravity, + this.switchOffsetY, + // ignore: non_constant_identifier_names + this.switchOffsetY_B, + this.logBtnOffsetY, + // ignore: non_constant_identifier_names + this.logBtnOffsetY_B, + this.logBtnWidth, + this.logBtnHeight, + this.logBtnOffsetX, + this.logBtnMarginLeftAndRight, + this.logBtnLayoutGravity, + this.privacyOffsetY, + // ignore: non_constant_identifier_names + this.privacyOffsetY_B, + // ignore: non_constant_identifier_names + this.sloganOffsetY_B, + this.checkBoxWidth, + this.checkBoxHeight, + this.checkboxHidden, + this.navTextSize, + this.logoWidth, + this.logoHeight, + this.switchAccTextSize, + this.switchAccText, + this.sloganTextSize, + this.sloganHidden, + this.uncheckedImgPath, + this.checkedImgPath, + this.privacyState = false, + this.protocolGravity, + this.privacyTextSize, + this.privacyMargin, + this.privacyBefore, + this.privacyEnd, + this.vendorPrivacyPrefix, + this.vendorPrivacySuffix, + this.tapAuthPageMaskClosePage = false, + this.dialogWidth, + this.dialogHeight, + this.dialogBottom, + this.dialogOffsetX, + this.dialogOffsetY, + this.dialogCornerRadiusArray, + this.pageBackgroundRadius, + this.webViewStatusBarColor, + this.webNavColor, + this.webNavTextColor, + this.webNavTextSize, + this.webNavReturnImgPath, + this.webSupportedJavascript, + this.authPageActIn, + this.activityOut, + this.authPageActOut, + this.activityIn, + this.screenOrientation, + this.logBtnToastHidden, + this.dialogAlpha, + this.privacyOperatorIndex, + this.privacyConectTexts, + this.protocolAction, + this.packageName, + this.loadingBackgroundPath, + this.isHiddenToast, + this.autoHideLoginLoading, + this.isHiddenCustom, + this.customThirdView, + this.backgroundColor, + /** + * "assets/background_gif.gif" + * "assets/background_gif1.gif" + * "assets/background_gif2.gif" + * "assets/background_image.jpeg" + * "assets/background_video.mp4" + * + * "https://upfile.asqql.com/2009pasdfasdfic2009s305985-ts/2018-7/20187232061776607.gif" + * "https://img.zcool.cn/community/01dda35912d7a3a801216a3e3675b3.gif", + */ + this.pageBackgroundPath = "assets/background_image.jpeg", + this.backgroundImageContentMode = ContentMode.scaleAspectFill, + this.bottomNavBarColor, + this.alertBarIsHidden, + this.alertTitleBarColor, + this.alertCloseItemIsHidden, + this.alertCloseImagePath, + this.alertCloseImageX, + this.alertCloseImageY, + this.alertCloseImageW, + this.alertCloseImageH, + this.alertBlurViewColor, + this.alertBlurViewAlpha, + this.presentDirection, + this.privacyAlertIsNeedShow = false, + this.privacyAlertIsNeedAutoLogin = true, + this.privacyAlertMaskIsNeedShow = true, + this.privacyAlertMaskAlpha = 0.5, + this.privacyAlertMaskColor, + this.privacyAlertAlpha = 1, + this.privacyAlertBackgroundColor, + this.privacyAlertEntryAnimation, + this.privacyAlertExitAnimation, + this.privacyAlertCornerRadiusArray, + this.privacyAlertAlignment, + this.privacyAlertWidth, + this.privacyAlertHeight, + this.privacyAlertOffsetX, + this.privacyAlertOffsetY, + this.privacyAlertTitleContent, + this.privacyAlertTitleBackgroundColor, + this.privacyAlertTitleAlignment, + this.privacyAlertTitleOffsetX, + this.privacyAlertTitleOffsetY, + this.privacyAlertTitleTextSize = 18, + this.privacyAlertTitleColor, + this.privacyAlertContentBackgroundColor, + this.privacyAlertContentTextSize = 16, + this.privacyAlertContentAlignment, + this.privacyAlertContentColor, + this.privacyAlertContentBaseColor, + this.privacyAlertProtocolNameUseUnderLine = false, + this.privacyAlertContentHorizontalMargin, + this.privacyAlertContentVerticalMargin, + this.privacyAlertBtnBackgroundImgPath, + this.privacyAlertBefore, + this.privacyAlertEnd, + this.privacyAlertBtnText, + this.privacyAlertBtnTextColor, + this.privacyAlertBtnTextSize = 18, + this.privacyAlertBtnWidth, + this.privacyAlertBtnHeigth, + this.privacyAlertCloseBtnShow = true, + this.privacyAlertCloseImagPath, + this.privacyAlertCloseScaleType, + this.privacyAlertCloseImgWidth, + this.privacyAlertCloseImgHeight, + + /// 授权页协议1文本颜色。 + this.privacyAlertOwnOneColor, + + /// 授权页协议2文本颜色。 + this.privacyAlertOwnTwoColor, + + /// 授权页协议3文本颜色。 + this.privacyAlertOwnThreeColor, + + /// 授权页运营商协议文本颜色。 + this.privacyAlertOperatorColor, + this.tapPrivacyAlertMaskCloseAlert = true, + this.autoQuitPage = true, + this.isHideToast = false, + this.toastText = '请先阅读用户协议', + this.toastBackground = '#FF000000', + this.toastColor = '#FFFFFFFF', + this.toastPadding = 9, + this.toastMarginTop = 0, + this.toastMarginBottom = 0, + this.toastPositionMode = 'bottom', + this.toastDelay = 3, + this.fullScreen=false, + this.authPageUseDayLight=false, + this.keepAllPageHideNavigationBar=false, + this.closeAuthPageReturnBack=false, + }) : assert(androidSk != null || iosSk != null), + assert(pageType != null), + assert(isDelay != null); + + Map toJson() => _$AliAuthModelToJson(this); +} + +Map _$AliAuthModelToJson(AliAuthModel instance) => + { + 'androidSk': instance.androidSk, + 'iosSk': instance.iosSk, + 'isDebug': instance.isDebug ?? false, + 'isDelay': instance.isDelay ?? false, + 'pageType': instance.pageType?.index ?? 0, + 'statusBarColor': instance.statusBarColor, + 'bottomNavColor': instance.bottomNavColor, + 'lightColor': instance.lightColor, + 'isStatusBarHidden': instance.isStatusBarHidden, + 'statusBarUIFlag': EnumUtils.formatUiFagValue(instance.statusBarUIFlag), + 'navColor': instance.navColor, + 'navText': instance.navText, + 'navTextColor': instance.navTextColor ?? "#000000", + 'navReturnImgPath': instance.navReturnImgPath, + 'navReturnImgWidth': instance.navReturnImgWidth, + 'navReturnImgHeight': instance.navReturnImgHeight, + 'customReturnBtn': instance.customReturnBtn?.toJson() ?? {}, + 'navReturnHidden': instance.navReturnHidden, + 'navReturnScaleType': instance.navReturnScaleType?.index ?? 0, + 'navHidden': instance.navHidden, + 'logoImgPath': instance.logoImgPath, + 'logoHidden': instance.logoHidden, + 'numberColor': instance.numberColor, + 'numberSize': instance.numberSize, + 'switchAccHidden': instance.switchAccHidden, + 'switchCheck': instance.switchCheck, + 'switchAccTextColor': instance.switchAccTextColor, + 'logBtnText': instance.logBtnText ?? "本机一键登录", + 'logBtnTextSize': instance.logBtnTextSize, + 'logBtnTextColor': instance.logBtnTextColor, + 'sloganTextColor': instance.sloganTextColor, + 'protocolOwnColor': instance.protocolOwnColor, + 'protocolOwnOneColor': instance.protocolOwnOneColor, + 'protocolOwnTwoColor': instance.protocolOwnTwoColor, + 'sloganText': instance.sloganText, + 'logBtnBackgroundPath': instance.logBtnBackgroundPath, + 'loadingImgPath': instance.loadingImgPath, + 'sloganOffsetY': instance.sloganOffsetY, + 'logoOffsetY': instance.logoOffsetY, + 'logoOffsetY_B': instance.logoOffsetY_B, + 'logoScaleType': instance.logoScaleType?.index ?? 2, + 'numFieldOffsetY': instance.numFieldOffsetY, + 'numFieldOffsetY_B': instance.numFieldOffsetY_B, + 'numberFieldOffsetX': instance.numberFieldOffsetX, + 'numberLayoutGravity': + EnumUtils.formatGravityValue(instance.numberLayoutGravity), + 'switchOffsetY': instance.switchOffsetY, + 'switchOffsetY_B': instance.switchOffsetY_B, + 'logBtnOffsetY': instance.logBtnOffsetY, + 'logBtnOffsetY_B': instance.logBtnOffsetY_B, + 'logBtnWidth': instance.logBtnWidth, + 'logBtnHeight': instance.logBtnHeight, + 'logBtnOffsetX': instance.logBtnOffsetX, + 'logBtnMarginLeftAndRight': instance.logBtnMarginLeftAndRight, + 'logBtnLayoutGravity': + EnumUtils.formatGravityValue(instance.logBtnLayoutGravity), + 'sloganOffsetY_B': instance.sloganOffsetY_B, + 'checkBoxWidth': instance.checkBoxWidth, + 'checkBoxHeight': instance.checkBoxHeight, + 'checkboxHidden': instance.checkboxHidden, + 'navTextSize': instance.navTextSize, + 'logoWidth': instance.logoWidth, + 'logoHeight': instance.logoHeight, + 'switchAccTextSize': instance.switchAccTextSize, + 'switchAccText': instance.switchAccText ?? "切换到其他方式", + 'sloganTextSize': instance.sloganTextSize, + 'sloganHidden': instance.sloganHidden, + 'uncheckedImgPath': instance.uncheckedImgPath, + 'checkedImgPath': instance.checkedImgPath, + 'vendorPrivacyPrefix': instance.vendorPrivacyPrefix ?? "《", + 'vendorPrivacySuffix': instance.vendorPrivacySuffix ?? "》", + 'tapAuthPageMaskClosePage': instance.tapAuthPageMaskClosePage ?? false, + 'dialogWidth': instance.dialogWidth, + 'dialogHeight': instance.dialogHeight, + 'dialogBottom': instance.dialogBottom ?? false, + 'dialogOffsetX': instance.dialogOffsetX, + 'dialogOffsetY': instance.dialogOffsetY, + 'dialogAlpha': instance.dialogAlpha, + 'dialogCornerRadiusArray': instance.dialogCornerRadiusArray, + 'webViewStatusBarColor': instance.webViewStatusBarColor, + 'webNavColor': instance.webNavColor, + 'webNavTextColor': instance.webNavTextColor, + 'webNavTextSize': instance.webNavTextSize, + 'webNavReturnImgPath': instance.webNavReturnImgPath, + 'webSupportedJavascript': instance.webSupportedJavascript, + 'authPageActIn': instance.authPageActIn, + 'activityOut': instance.activityOut, + 'authPageActOut': instance.authPageActOut, + 'activityIn': instance.activityIn, + 'screenOrientation': instance.screenOrientation, + 'logBtnToastHidden': instance.logBtnToastHidden, + 'pageBackgroundRadius': instance.pageBackgroundRadius, + 'protocolOneName': instance.protocolOneName, + 'protocolOneURL': instance.protocolOneURL, + 'protocolTwoName': instance.protocolTwoName, + 'protocolTwoURL': instance.protocolTwoURL, + 'protocolColor': instance.protocolColor, + 'protocolLayoutGravity': + EnumUtils.formatGravityValue(instance.protocolLayoutGravity), + 'protocolThreeName': instance.protocolThreeName, + 'protocolThreeURL': instance.protocolThreeURL, + 'protocolCustomColor': instance.protocolCustomColor, + 'protocolAction': instance.protocolAction, + 'privacyState': instance.privacyState, + 'protocolGravity': EnumUtils.formatGravityValue(instance.protocolGravity), + 'privacyOffsetY': instance.privacyOffsetY, + 'privacyOffsetY_B': instance.privacyOffsetY_B, + 'privacyTextSize': instance.privacyTextSize, + 'privacyMargin': instance.privacyMargin, + 'privacyBefore': instance.privacyBefore ?? "我已阅读并同意", + 'privacyEnd': instance.privacyEnd, + 'packageName': instance.packageName, + 'privacyOperatorIndex': instance.privacyOperatorIndex, + 'privacyConectTexts': instance.privacyConectTexts ?? [",", "", "和"], + 'isHiddenCustom': instance.isHiddenCustom, + 'customThirdView': instance.customThirdView?.toJson() ?? {}, + 'backgroundColor': instance.backgroundColor ?? "#000000", + 'pageBackgroundPath': instance.pageBackgroundPath, + 'backgroundImageContentMode': + instance.backgroundImageContentMode?.index ?? 0, + 'bottomNavBarColor': instance.bottomNavBarColor, + 'alertBarIsHidden': instance.alertBarIsHidden, + 'alertTitleBarColor': + instance.alertTitleBarColor ?? instance.navColor ?? "#ffffff", + 'alertCloseItemIsHidden': instance.alertCloseItemIsHidden, + 'alertCloseImagePath': instance.alertCloseImagePath, + 'alertCloseImageX': instance.alertCloseImageX, + 'alertCloseImageY': instance.alertCloseImageY, + 'alertCloseImageW': instance.alertCloseImageW, + 'alertCloseImageH': instance.alertCloseImageH, + 'alertBlurViewColor': instance.alertBlurViewColor ?? "#000000", + 'alertBlurViewAlpha': instance.alertBlurViewAlpha ?? 0.5, + 'presentDirection': instance.presentDirection, + 'privacyAlertIsNeedShow': instance.privacyAlertIsNeedShow ?? false, + 'privacyAlertIsNeedAutoLogin': instance.privacyAlertIsNeedAutoLogin, + 'privacyAlertMaskIsNeedShow': instance.privacyAlertMaskIsNeedShow, + 'privacyAlertMaskAlpha': instance.privacyAlertMaskAlpha, + 'privacyAlertMaskColor': instance.privacyAlertMaskColor ?? "#000000", + 'privacyAlertAlpha': instance.privacyAlertAlpha, + 'privacyAlertBackgroundColor': + instance.privacyAlertBackgroundColor ?? "#ffffff", + 'privacyAlertEntryAnimation': instance.privacyAlertEntryAnimation, + 'privacyAlertExitAnimation': instance.privacyAlertExitAnimation, + 'privacyAlertCornerRadiusArray': + instance.privacyAlertCornerRadiusArray ?? [10, 10, 10, 10], + 'privacyAlertAlignment': + EnumUtils.formatGravityValue(instance.privacyAlertAlignment), + 'privacyAlertWidth': instance.privacyAlertWidth, + 'privacyAlertHeight': instance.privacyAlertHeight, + 'privacyAlertOffsetX': instance.privacyAlertOffsetX, + 'privacyAlertOffsetY': instance.privacyAlertOffsetY, + 'privacyAlertTitleContent': + instance.privacyAlertTitleContent ?? "请阅读并同意以下条款", + 'privacyAlertTitleBackgroundColor': + instance.privacyAlertTitleBackgroundColor ?? "#ffffff", + 'privacyAlertTitleAlignment': EnumUtils.formatGravityValue( + instance.privacyAlertTitleAlignment ?? Gravity.centerHorizntal), + 'privacyAlertTitleOffsetX': instance.privacyAlertTitleOffsetX, + 'privacyAlertTitleOffsetY': instance.privacyAlertTitleOffsetY, + 'privacyAlertTitleTextSize': instance.privacyAlertTitleTextSize ?? 22, + 'privacyAlertTitleColor': instance.privacyAlertTitleColor ?? "#000000", + 'privacyAlertContentBackgroundColor': + instance.privacyAlertContentBackgroundColor ?? "#ffffff", + 'privacyAlertContentTextSize': instance.privacyAlertContentTextSize ?? 12, + 'privacyAlertContentAlignment': + EnumUtils.formatGravityValue(instance.privacyAlertContentAlignment), + 'privacyAlertContentColor': instance.privacyAlertContentColor, + 'privacyAlertContentBaseColor': instance.privacyAlertContentBaseColor, + 'privacyAlertProtocolNameUseUnderLine': + instance.privacyAlertProtocolNameUseUnderLine, + 'privacyAlertContentHorizontalMargin': + instance.privacyAlertContentHorizontalMargin, + 'privacyAlertContentVerticalMargin': + instance.privacyAlertContentVerticalMargin ?? 10, + 'privacyAlertBtnBackgroundImgPath': + instance.privacyAlertBtnBackgroundImgPath ?? "", + 'privacyAlertBefore': instance.privacyAlertBefore ?? "", + 'privacyAlertEnd': instance.privacyAlertEnd ?? "", + 'privacyAlertBtnText': instance.privacyAlertBtnText ?? "同意并登录", + 'privacyAlertBtnTextColor': instance.privacyAlertBtnTextColor, + 'privacyAlertBtnTextSize': instance.privacyAlertBtnTextSize ?? 10, + 'privacyAlertBtnWidth': instance.privacyAlertBtnWidth, + 'privacyAlertBtnHeigth': instance.privacyAlertBtnHeigth, + 'privacyAlertCloseBtnShow': instance.privacyAlertCloseBtnShow, + 'privacyAlertCloseImagPath': instance.privacyAlertCloseImagPath, + 'privacyAlertCloseScaleType': + instance.privacyAlertCloseScaleType?.index ?? 0, + 'privacyAlertCloseImgWidth': instance.privacyAlertCloseImgWidth, + 'privacyAlertCloseImgHeight': instance.privacyAlertCloseImgHeight, + 'privacyAlertOwnOneColor': instance.privacyAlertOwnOneColor, + 'privacyAlertOwnTwoColor': instance.privacyAlertOwnTwoColor, + 'privacyAlertOwnThreeColor': instance.privacyAlertOwnThreeColor, + 'tapPrivacyAlertMaskCloseAlert': instance.tapPrivacyAlertMaskCloseAlert, + 'isHiddenToast': instance.isHiddenToast, + 'autoHideLoginLoading': instance.autoHideLoginLoading ?? true, + 'autoQuitPage': instance.autoQuitPage, + 'isHideToast': instance.isHideToast, + 'toastText': instance.toastText, + 'toastBackground': instance.toastBackground, + 'toastColor': instance.toastColor, + 'toastPadding': instance.toastPadding, + 'toastMarginTop': instance.toastMarginTop, + 'toastMarginBottom': instance.toastMarginBottom, + 'toastPositionMode': instance.toastPositionMode, + 'toastDelay': instance.toastDelay, + 'fullScreen': instance.fullScreen ?? false, + 'authPageUseDayLight': instance.authPageUseDayLight ?? false, + 'keepAllPageHideNavigationBar': instance.keepAllPageHideNavigationBar ?? false, + 'closeAuthPageReturnBack': instance.closeAuthPageReturnBack ?? false, + }; + +/// 初始配置&注意事项 +/// 所有关于路径的字段需要在android/app/src/main/res/drawable 或者 drawable-xxxxxx 目录下有对应资源 +/// 所有设置的大小都为dp 或者 单位 如果px 单位需要转换 +/// 所有颜色设置为 十六进制颜色代码 加上两位数的透明度 例如 #00ffffff 为透明 #ffffff为白色 +/// 当设置参数isdialog为false时 dialog 相关设置参数设置无效 +/// 默认开启自定义第三方布局 加载文件为android/app/src/main/res/layout/custom_login.xml 名称的xml布局文件 如果自定义,修改改文件即可 +/// 在自定义登录布局中点击事件返回的状态吗统一为returnCode:700005 returnData:点击的第几个按钮 // 具体看md +/// 参数dialogOffsetX dialogOffsetY 设置为-1 默认为居中 +/// 关于弹窗的梦层设置 android/app/src/main/res/value/style.xml authsdk_activity_dialog参数设置 +/// 当开启customPageBackgroundLyout 参数时 请确保layout 文件夹下有custom_page_background 名称布局文件,否则加载默认布局文件 +/// ios 当开启customPageBackgroundLyout时 navReturnImgPath navReturnImgWidth/navReturnImgHeight理论最大高度45左右参数为必须参数否则报错 +/// 'appPrivacyOne'、'appPrivacyTwo' 字段中的逗号拼接处请勿使用多余的空格,以免出现未知错误 +/// dialogBottom 为false时 默认水平垂直居中 +/// 如果需要修改弹窗的圆角背景可修改android/app/src/main/res/drawable/dialog_background_color.xml 文件 +/// 'appPrivacyOne'、'appPrivacyTwo' 字段中的逗号拼接处请勿使用多余的空格,以免出现未知错误 diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_platform_interface.dart b/airhub_app/packages/ali_auth/lib/ali_auth_platform_interface.dart new file mode 100644 index 0000000..7f51132 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_platform_interface.dart @@ -0,0 +1,118 @@ +import 'package:ali_auth/ali_auth_model.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'ali_auth_method_channel.dart' + if (dart.library.html) "ali_auth_web.dart"; + +abstract class AliAuthPlatform extends PlatformInterface { + /// Constructs a AliAuthPlatform. + AliAuthPlatform() : super(token: _token); + + static final Object _token = Object(); + + static AliAuthPlatform _instance = MethodChannelAliAuth(); + + /// The default instance of [AliAuthPlatform] to use. + /// + /// Defaults to [MethodChannelAliAuth]. + static AliAuthPlatform get instance => _instance; + + Future? get appleLogin => null; + + Future? get checkCellularDataEnable => null; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [AliAuthPlatform] when + /// they register themselves. + static set instance(AliAuthPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future getSdkVersion() { + throw UnimplementedError('getSdkVersion() has not been implemented.'); + } + + Stream? onChange({bool type = true}) { + throw UnimplementedError('onChange() has not been implemented.'); + } + + resume() { + throw UnimplementedError('resume() has not been implemented.'); + } + + dispose() { + throw UnimplementedError('dispose() has not been implemented.'); + } + + pause() { + throw UnimplementedError('pause() has not been implemented.'); + } + + Future openPage(String? pageRoute) { + throw UnimplementedError('openPage() has not been implemented.'); + } + + Future quitPage() { + throw UnimplementedError('quitPage() has not been implemented.'); + } + + Future checkEnvAvailable() { + throw UnimplementedError('checkEnvAvailable() has not been implemented.'); + } + + Future queryCheckBoxIsChecked() { + throw UnimplementedError('queryCheckBoxIsChecked() has not been implemented.'); + } + + Future setCheckboxIsChecked() { + throw UnimplementedError('setCheckboxIsChecked() has not been implemented.'); + } + + Future hideLoading() { + throw UnimplementedError('quitPage() has not been implemented.'); + } + + Future getCurrentCarrierName() { + throw UnimplementedError('quitPage() has not been implemented.'); + } + + Future login({int timeout = 5000}) { + throw UnimplementedError('login() has not been implemented.'); + } + + Future initSdk(AliAuthModel? config) { + throw UnimplementedError('login() has not been implemented.'); + } + + loginListen( + {bool type = true, + required Function onEvent, + Function? onError, + isOnlyOne = true}) { + throw UnimplementedError('loginListen() has not been implemented.'); + } + + Future getConnection() { + throw UnimplementedError('getSdkVersion() has not been implemented.'); + } + + Future setLoggerEnable(bool isEnable) { + throw UnimplementedError('getSdkVersion() has not been implemented.'); + } + + /// 调用之前先去用户服务端获取accessToken和jwtToken + Future checkAuthAvailable(String accessToken, String jwtToken, + Function(dynamic) success, Function(dynamic) error) async { + throw UnimplementedError('getSdkVersion() has not been implemented.'); + } + + /// 身份鉴权成功后才可调用获取Token接口。 + Future getVerifyToken( + Function(dynamic) success, Function(dynamic) error) async { + throw UnimplementedError('getSdkVersion() has not been implemented.'); + } +} diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_web.dart b/airhub_app/packages/ali_auth/lib/ali_auth_web.dart new file mode 100644 index 0000000..7232087 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_web.dart @@ -0,0 +1,48 @@ +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'; + +/// A web implementation of the AliAuthPlatform of the AliAuth plugin. +class AliAuthPluginWeb extends AliAuthPlatform { + AliAuthPluginWeb(); + + AliAuthPluginWebApi aliAuthPluginWebApi = AliAuthPluginWebApi(); + + static void registerWith(Registrar registrar) { + AliAuthPlatform.instance = AliAuthPluginWeb(); + } + + @override + Future getPlatformVersion() async { + return 'web'; + } + + @override + Future getSdkVersion() async { + return await aliAuthPluginWebApi.getVersion(); + } + + @override + Future getConnection() async { + return await aliAuthPluginWebApi.getConnection(); + } + + @override + Future setLoggerEnable(bool isEnable) async { + return await aliAuthPluginWebApi.setLoggerEnable(isEnable); + } + + @override + Future checkAuthAvailable(String accessToken, String jwtToken, + Function(dynamic) success, Function(dynamic) error) async { + aliAuthPluginWebApi.checkAuthAvailable( + accessToken, jwtToken, success, error); + } + + @override + Future getVerifyToken( + Function(dynamic) success, Function(dynamic) error) async { + aliAuthPluginWebApi.getVerifyToken(success, error); + } +} diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_web_api.dart b/airhub_app/packages/ali_auth/lib/ali_auth_web_api.dart new file mode 100644 index 0000000..e652473 --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_web_api.dart @@ -0,0 +1,16 @@ +/// Web stub for ali_auth — provides matching API surface without dart:js_interop @JS classes. +/// These are never actually called since the app uses conditional imports to skip ali_auth on web. + +class AliAuthPluginWebApi { + Future getConnection() async => null; + + Future setLoggerEnable(bool isEnable) async {} + + Future getVersion() async => null; + + Future checkAuthAvailable(String accessToken, String jwtToken, + Function(dynamic ststus) success, Function(dynamic ststus) error) async {} + + Future getVerifyToken( + Function(dynamic ststus) success, Function(dynamic ststus) error) async {} +} diff --git a/airhub_app/packages/ali_auth/lib/ali_auth_web_utils.dart b/airhub_app/packages/ali_auth/lib/ali_auth_web_utils.dart new file mode 100644 index 0000000..32f171a --- /dev/null +++ b/airhub_app/packages/ali_auth/lib/ali_auth_web_utils.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:js_util'; +import 'dart:js_interop'; + +typedef Func1 = R Function(A a); + +@JS('JSON.stringify') +external String stringify(Object obj); + +@JS('console.log') +external void log(Object obj); + +@JS('alert') +external void alert(Object obj); + +@JS('Promise') +class PromiseJsImpl extends ThenableJsImpl { + external PromiseJsImpl(Function resolver); + external static PromiseJsImpl all(List values); + external static PromiseJsImpl reject(error); + external static PromiseJsImpl resolve(value); +} + +@anonymous +@JS() +abstract class ThenableJsImpl { + external ThenableJsImpl then([Func1 onResolve, Func1 onReject]); +} + +Future handleThenable(ThenableJsImpl thenable) => + promiseToFuture(thenable); diff --git a/airhub_app/packages/ali_auth/pubspec.lock b/airhub_app/packages/ali_auth/pubspec.lock new file mode 100644 index 0000000..f724d0a --- /dev/null +++ b/airhub_app/packages/ali_auth/pubspec.lock @@ -0,0 +1,218 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: "direct main" + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.8.0 <=3.38.9" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/airhub_app/packages/ali_auth/pubspec.yaml b/airhub_app/packages/ali_auth/pubspec.yaml new file mode 100644 index 0000000..bfdcd91 --- /dev/null +++ b/airhub_app/packages/ali_auth/pubspec.yaml @@ -0,0 +1,78 @@ +name: ali_auth +description: This is a plug-in for one click login in the alicloud number authentication service. Alibaba cloud is also used in the one click login function +version: 1.3.7 +homepage: https://github.com/CodeGather/flutter_ali_auth +repository: https://github.com/CodeGather/flutter_ali_auth/tree/master/example +issue_tracker: https://github.com/CodeGather/flutter_ali_auth/issues +topics: [aliyun, phone] + +environment: + sdk: ">=2.19.0 <=3.38.9" + flutter: ">=3.16.8" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + plugin_platform_interface: ^2.1.8 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.sean.rao.ali_auth + pluginClass: AliAuthPlugin + ios: + pluginClass: AliAuthPlugin + web: + pluginClass: AliAuthPluginWeb + fileName: ali_auth_web.dart + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages + +screenshots: + - description: The flutter ali_auth package logo. + path: screenshots/logo.png \ No newline at end of file diff --git a/airhub_app/pubspec.lock b/airhub_app/pubspec.lock index 6eef25d..2d4f160 100644 --- a/airhub_app/pubspec.lock +++ b/airhub_app/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "91.0.0" analyzer: @@ -14,7 +14,7 @@ packages: description: name: analyzer sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.4.0" analyzer_buffer: @@ -22,7 +22,7 @@ packages: description: name: analyzer_buffer sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.11" analyzer_plugin: @@ -30,7 +30,7 @@ packages: description: name: analyzer_plugin sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.10" args: @@ -38,7 +38,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" async: @@ -46,7 +46,7 @@ packages: description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.13.0" audio_session: @@ -54,7 +54,7 @@ packages: description: name: audio_session sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.25" bluez: @@ -62,7 +62,7 @@ packages: description: name: bluez sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.3" boolean_selector: @@ -70,7 +70,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" build: @@ -78,7 +78,7 @@ packages: description: name: build sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.4" build_config: @@ -86,7 +86,7 @@ packages: description: name: build_config sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.0" build_daemon: @@ -94,7 +94,7 @@ packages: description: name: build_daemon sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.1" build_runner: @@ -102,7 +102,7 @@ packages: description: name: build_runner sha256: ac78098de97893812b7aff1154f29008fa2464cad9e8e7044d39bc905dad4fbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.0" built_collection: @@ -110,7 +110,7 @@ packages: description: name: built_collection sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: @@ -118,7 +118,7 @@ packages: description: name: built_value sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.12.3" characters: @@ -126,7 +126,7 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" checked_yaml: @@ -134,7 +134,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.4" ci: @@ -142,7 +142,7 @@ packages: description: name: ci sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.0" cli_config: @@ -150,7 +150,7 @@ packages: description: name: cli_config sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" cli_util: @@ -158,7 +158,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.2" clock: @@ -166,7 +166,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" code_assets: @@ -174,7 +174,7 @@ packages: description: name: code_assets sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" code_builder: @@ -182,7 +182,7 @@ packages: description: name: code_builder sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.11.1" collection: @@ -190,7 +190,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" convert: @@ -198,7 +198,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.2" coverage: @@ -206,7 +206,7 @@ packages: description: name: coverage sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.15.0" cross_file: @@ -214,7 +214,7 @@ packages: description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.5+2" crypto: @@ -222,7 +222,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" custom_lint: @@ -230,7 +230,7 @@ packages: description: name: custom_lint sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.1" custom_lint_builder: @@ -238,7 +238,7 @@ packages: description: name: custom_lint_builder sha256: "1128db6f58e71d43842f3b9be7465c83f0c47f4dd8918f878dd6ad3b72a32072" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.1" custom_lint_core: @@ -246,7 +246,7 @@ packages: description: name: custom_lint_core sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.1" custom_lint_visitor: @@ -254,7 +254,7 @@ packages: description: name: custom_lint_visitor sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0+8.4.0" dart_style: @@ -262,7 +262,7 @@ packages: description: name: dart_style sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" dbus: @@ -270,15 +270,31 @@ packages: description: name: dbus sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.12" + dio: + dependency: "direct main" + description: + name: dio + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + url: "https://pub.dev" + source: hosted + version: "5.9.1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.3" ffi: @@ -286,7 +302,7 @@ packages: description: name: ffi sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" file: @@ -294,7 +310,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" file_selector_linux: @@ -302,7 +318,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.4" file_selector_macos: @@ -310,7 +326,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -318,7 +334,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" file_selector_windows: @@ -326,7 +342,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.3+5" fixnum: @@ -334,7 +350,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter: @@ -347,7 +363,7 @@ packages: description: name: flutter_blue_plus sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.36.8" flutter_blue_plus_android: @@ -355,7 +371,7 @@ packages: description: name: flutter_blue_plus_android sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.4" flutter_blue_plus_darwin: @@ -363,7 +379,7 @@ packages: description: name: flutter_blue_plus_darwin sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.3" flutter_blue_plus_linux: @@ -371,7 +387,7 @@ packages: description: name: flutter_blue_plus_linux sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.3" flutter_blue_plus_platform_interface: @@ -379,7 +395,7 @@ packages: description: name: flutter_blue_plus_platform_interface sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.0" flutter_blue_plus_web: @@ -387,7 +403,7 @@ packages: description: name: flutter_blue_plus_web sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.2" flutter_lints: @@ -395,7 +411,7 @@ packages: description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_localizations: @@ -408,7 +424,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.33" flutter_riverpod: @@ -416,7 +432,7 @@ packages: description: name: flutter_riverpod sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" flutter_svg: @@ -424,7 +440,7 @@ packages: description: name: flutter_svg sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.3" flutter_test: @@ -442,7 +458,7 @@ packages: description: name: fpdart sha256: f8e9d0989ba293946673e382c59ac513e30cb6746a9452df195f29e3357a73d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.0" freezed: @@ -450,7 +466,7 @@ packages: description: name: freezed sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.3" freezed_annotation: @@ -458,7 +474,7 @@ packages: description: name: freezed_annotation sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.0" frontend_server_client: @@ -466,7 +482,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.0" glob: @@ -474,7 +490,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" go_router: @@ -482,7 +498,7 @@ packages: description: name: go_router sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "14.8.1" google_fonts: @@ -490,7 +506,7 @@ packages: description: name: google_fonts sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.3" graphs: @@ -498,7 +514,7 @@ packages: description: name: graphs sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.2" hooks: @@ -506,7 +522,7 @@ packages: description: name: hooks sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" hotreloader: @@ -514,7 +530,7 @@ packages: description: name: hotreloader sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.0" http: @@ -522,7 +538,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.0" http_multi_server: @@ -530,7 +546,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" http_parser: @@ -538,7 +554,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.2" image_picker: @@ -546,7 +562,7 @@ packages: description: name: image_picker sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" image_picker_android: @@ -554,7 +570,7 @@ packages: description: name: image_picker_android sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.13+13" image_picker_for_web: @@ -562,7 +578,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.1" image_picker_ios: @@ -570,7 +586,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.13+6" image_picker_linux: @@ -578,7 +594,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" image_picker_macos: @@ -586,7 +602,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -594,7 +610,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.1" image_picker_windows: @@ -602,7 +618,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" intl: @@ -610,7 +626,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.20.2" io: @@ -618,7 +634,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" js: @@ -626,7 +642,7 @@ packages: description: name: js sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.2" json_annotation: @@ -634,7 +650,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.9.0" json_serializable: @@ -642,7 +658,7 @@ packages: description: name: json_serializable sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.11.2" just_audio: @@ -650,7 +666,7 @@ packages: description: name: just_audio sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.46" just_audio_platform_interface: @@ -658,7 +674,7 @@ packages: description: name: just_audio_platform_interface sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.6.0" just_audio_web: @@ -666,7 +682,7 @@ packages: description: name: just_audio_web sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.16" leak_tracker: @@ -674,7 +690,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -682,7 +698,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.10" leak_tracker_testing: @@ -690,7 +706,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" lints: @@ -698,7 +714,7 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.0" logging: @@ -706,7 +722,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" matcher: @@ -714,7 +730,7 @@ packages: description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.17" material_color_utilities: @@ -722,7 +738,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.11.1" meta: @@ -730,7 +746,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.17.0" mime: @@ -738,7 +754,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" mockito: @@ -746,7 +762,7 @@ packages: description: name: mockito sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.6.3" native_toolchain_c: @@ -754,7 +770,7 @@ packages: description: name: native_toolchain_c sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.17.4" node_preamble: @@ -762,7 +778,7 @@ packages: description: name: node_preamble sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" objective_c: @@ -770,7 +786,7 @@ packages: description: name: objective_c sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.3.0" package_config: @@ -778,7 +794,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" path: @@ -786,7 +802,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" path_parsing: @@ -794,7 +810,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" path_provider: @@ -802,7 +818,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_android: @@ -810,7 +826,7 @@ packages: description: name: path_provider_android sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.22" path_provider_foundation: @@ -818,7 +834,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.6.0" path_provider_linux: @@ -826,7 +842,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -834,7 +850,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" path_provider_windows: @@ -842,7 +858,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" permission_handler: @@ -850,7 +866,7 @@ packages: description: name: permission_handler sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.4.0" permission_handler_android: @@ -858,7 +874,7 @@ packages: description: name: permission_handler_android sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "12.1.0" permission_handler_apple: @@ -866,7 +882,7 @@ packages: description: name: permission_handler_apple sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.4.7" permission_handler_html: @@ -874,7 +890,7 @@ packages: description: name: permission_handler_html sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.3+5" permission_handler_platform_interface: @@ -882,7 +898,7 @@ packages: description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.0" permission_handler_windows: @@ -890,7 +906,7 @@ packages: description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.1" petitparser: @@ -898,7 +914,7 @@ packages: description: name: petitparser sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" platform: @@ -906,7 +922,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.6" plugin_platform_interface: @@ -914,7 +930,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" pool: @@ -922,7 +938,7 @@ packages: description: name: pool sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.2" pub_semver: @@ -930,7 +946,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" pubspec_parse: @@ -938,7 +954,7 @@ packages: description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.0" riverpod: @@ -946,7 +962,7 @@ packages: description: name: riverpod sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" riverpod_analyzer_utils: @@ -954,7 +970,7 @@ packages: description: name: riverpod_analyzer_utils sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0-dev.7" riverpod_annotation: @@ -962,7 +978,7 @@ packages: description: name: riverpod_annotation sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" riverpod_generator: @@ -970,7 +986,7 @@ packages: description: name: riverpod_generator sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" riverpod_lint: @@ -978,7 +994,7 @@ packages: description: name: riverpod_lint sha256: "7ef9c43469e9b5ac4e4c3b24d7c30642e47ce1b12cd7dcdd643534db0a72ed13" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" rxdart: @@ -986,15 +1002,71 @@ packages: description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + url: "https://pub.dev" + source: hosted + version: "2.4.20" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.2" shelf_packages_handler: @@ -1002,7 +1074,7 @@ packages: description: name: shelf_packages_handler sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" shelf_static: @@ -1010,7 +1082,7 @@ packages: description: name: shelf_static sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.3" shelf_web_socket: @@ -1018,7 +1090,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" sky_engine: @@ -1031,7 +1103,7 @@ packages: description: name: source_gen sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.2.0" source_helper: @@ -1039,7 +1111,7 @@ packages: description: name: source_helper sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.8" source_map_stack_trace: @@ -1047,7 +1119,7 @@ packages: description: name: source_map_stack_trace sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" source_maps: @@ -1055,7 +1127,7 @@ packages: description: name: source_maps sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.10.13" source_span: @@ -1063,7 +1135,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.2" stack_trace: @@ -1071,7 +1143,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" state_notifier: @@ -1079,7 +1151,7 @@ packages: description: name: state_notifier sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" stream_channel: @@ -1087,7 +1159,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" stream_transform: @@ -1095,7 +1167,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" string_scanner: @@ -1103,7 +1175,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" synchronized: @@ -1111,7 +1183,7 @@ packages: description: name: synchronized sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.0" term_glyph: @@ -1119,7 +1191,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test: @@ -1127,7 +1199,7 @@ packages: description: name: test sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.26.3" test_api: @@ -1135,7 +1207,7 @@ packages: description: name: test_api sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.7" test_core: @@ -1143,7 +1215,7 @@ packages: description: name: test_core sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.12" typed_data: @@ -1151,7 +1223,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" uuid: @@ -1159,7 +1231,7 @@ packages: description: name: uuid sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.5.2" vector_graphics: @@ -1167,7 +1239,7 @@ packages: description: name: vector_graphics sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.19" vector_graphics_codec: @@ -1175,7 +1247,7 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.13" vector_graphics_compiler: @@ -1183,7 +1255,7 @@ packages: description: name: vector_graphics_compiler sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.20" vector_math: @@ -1191,7 +1263,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" vm_service: @@ -1199,7 +1271,7 @@ packages: description: name: vm_service sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "15.0.2" watcher: @@ -1207,7 +1279,7 @@ packages: description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" web: @@ -1215,7 +1287,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" web_socket: @@ -1223,7 +1295,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" web_socket_channel: @@ -1231,7 +1303,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" webkit_inspection_protocol: @@ -1239,7 +1311,7 @@ packages: description: name: webkit_inspection_protocol sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" webview_flutter: @@ -1247,7 +1319,7 @@ packages: description: name: webview_flutter sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.13.1" webview_flutter_android: @@ -1255,7 +1327,7 @@ packages: description: name: webview_flutter_android sha256: eeeb3fcd5f0ff9f8446c9f4bbc18a99b809e40297528a3395597d03aafb9f510 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.10.11" webview_flutter_platform_interface: @@ -1263,7 +1335,7 @@ packages: description: name: webview_flutter_platform_interface sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.14.0" webview_flutter_wkwebview: @@ -1271,7 +1343,7 @@ packages: description: name: webview_flutter_wkwebview sha256: "0412b657a2828fb301e73509909e6ec02b77cd2b441ae9f77125d482b3ddf0e7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.23.6" xdg_directories: @@ -1279,7 +1351,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" xml: @@ -1287,7 +1359,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.6.1" yaml: @@ -1295,7 +1367,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" sdks: diff --git a/airhub_app/pubspec.yaml b/airhub_app/pubspec.yaml index f393c9c..e6a5f11 100644 --- a/airhub_app/pubspec.yaml +++ b/airhub_app/pubspec.yaml @@ -50,6 +50,13 @@ dependencies: json_annotation: ^4.9.0 fpdart: ^1.1.0 # Functional programming (Optional/Recommended) + # Network & Storage + dio: ^5.7.0 + shared_preferences: ^2.3.0 + + # Aliyun Phone Auth (一键登录) — 本地 Web 调试时禁用 + # ali_auth: ^1.3.7 + # Existing dependencies webview_flutter: ^4.4.2 permission_handler: ^11.0.0 diff --git a/airhub_app/test/widget_test.dart b/airhub_app/test/widget_test.dart index 746d912..bd743ae 100644 --- a/airhub_app/test/widget_test.dart +++ b/airhub_app/test/widget_test.dart @@ -1,30 +1,13 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:airhub_app/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + testWidgets('App smoke test', (WidgetTester tester) async { + await tester.pumpWidget( + const ProviderScope(child: AirhubApp()), + ); + await tester.pumpAndSettle(); }); } diff --git a/docs/API_SPECIFICATION.md b/docs/API_SPECIFICATION.md new file mode 100644 index 0000000..2705c6e --- /dev/null +++ b/docs/API_SPECIFICATION.md @@ -0,0 +1,1202 @@ +# Airhub 后台接口规范文档 + +> 版本: 1.0.0 +> 更新日期: 2025-02-09 +> 基于 PRD HTML 原型与 airhub_app Flutter 代码分析整理 + +--- + +## 目录 + +- [概述](#概述) +- [通用规范](#通用规范) +- [接口列表](#接口列表) + - [1. 用户认证](#1-用户认证) + - [2. 用户信息](#2-用户信息) + - [3. 设备管理](#3-设备管理) + - [4. 角色记忆](#4-角色记忆) + - [5. 故事模块](#5-故事模块) + - [6. 音乐模块](#6-音乐模块) + - [7. 通知模块](#7-通知模块) + - [8. 系统接口](#8-系统接口) +- [数据模型](#数据模型) +- [开发优先级](#开发优先级) +- [附录:代码对照表](#附录代码对照表) + +--- + +## 概述 + +本文档定义了 Airhub 智能硬件控制中心 APP 所需的全部后台接口。接口设计基于以下来源: + +1. **PRD HTML 原型文件** - 根目录下的 `.html` 文件 +2. **airhub_app Flutter 代码** - `airhub_app/lib/` 目录 +3. **已实现的接口调用** - `music-creation.html` 中的实际 API 调用 + +### 技术栈建议 + +- 后端框架: Node.js (Express/Fastify) 或 Python (FastAPI) +- 数据库: PostgreSQL / MySQL +- 缓存: Redis +- 对象存储: 阿里云 OSS +- 实时通信: SSE (Server-Sent Events) + +--- + +## 通用规范 + +### 基础 URL + +``` +开发环境: http://localhost:3000/api +生产环境: https://api.airhub.com/v1 +``` + +### 请求头 + +```http +Content-Type: application/json +Authorization: Bearer {access_token} +X-Device-Id: {device_uuid} +X-App-Version: 1.0.0 +``` + +### 响应格式 + +**成功响应** +```json +{ + "code": 0, + "message": "success", + "data": { ... } +} +``` + +**错误响应** +```json +{ + "code": 40001, + "message": "参数错误", + "data": null +} +``` + +### 错误码定义 + +| 错误码 | 说明 | +|--------|------| +| 0 | 成功 | +| 40001 | 参数错误 | +| 40101 | 未授权 (Token 无效) | +| 40102 | Token 过期 | +| 40301 | 无权限 | +| 40401 | 资源不存在 | +| 50001 | 服务器内部错误 | + +--- + +## 接口列表 + +### 1. 用户认证 + +#### 1.1 发送验证码 + +```http +POST /api/auth/send-code +``` + +**请求参数** +```json +{ + "phone": "13800138000" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "验证码已发送", + "data": { + "expire_in": 60 + } +} +``` + +**说明**: 验证码 60 秒内有效,同一手机号 60 秒内只能发送一次 + +--- + +#### 1.2 验证码登录 + +```http +POST /api/auth/login +``` + +**请求参数** +```json +{ + "phone": "13800138000", + "code": "123456" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "登录成功", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "expire_in": 604800, + "user": { + "id": "u_12345678", + "phone": "138****8000", + "nickname": "用户8000", + "avatar_url": null + } + } +} +``` + +**Flutter 代码对应**: `AuthRemoteDataSource.loginWithPhone()` + +--- + +#### 1.3 一键登录 + +```http +POST /api/auth/one-click-login +``` + +**请求参数** +```json +{ + "access_token": "运营商SDK返回的token" +} +``` + +**响应**: 同验证码登录 + +**说明**: 使用运营商 SDK (如阿里云号码认证) 获取本机号码 + +**Flutter 代码对应**: `AuthRemoteDataSource.oneClickLogin()` + +--- + +#### 1.4 刷新 Token + +```http +POST /api/auth/refresh-token +``` + +**请求参数** +```json +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**响应** +```json +{ + "code": 0, + "data": { + "access_token": "新的access_token", + "expire_in": 604800 + } +} +``` + +--- + +#### 1.5 退出登录 + +```http +POST /api/auth/logout +``` + +**请求头**: 需要 Authorization + +**响应** +```json +{ + "code": 0, + "message": "已退出登录" +} +``` + +**Flutter 代码对应**: `AuthRepository.logout()` + +--- + +#### 1.6 账号注销 + +```http +DELETE /api/auth/account +``` + +**请求头**: 需要 Authorization + +**响应** +```json +{ + "code": 0, + "message": "账号注销申请已提交,将在7个工作日内处理" +} +``` + +**PRD 来源**: `settings.html` - 账号注销功能 + +--- + +### 2. 用户信息 + +#### 2.1 获取用户资料 + +```http +GET /api/user/profile +``` + +**响应** +```json +{ + "code": 0, + "data": { + "id": "u_12345678", + "phone": "138****8000", + "nickname": "土豆", + "avatar_url": "https://oss.airhub.com/avatars/xxx.jpg", + "gender": "男", + "birthday": "1994-12-09", + "created_at": "2025-01-15T10:30:00Z" + } +} +``` + +**Flutter 代码对应**: `profile_page.dart` - 用户卡片显示 + +--- + +#### 2.2 更新用户资料 + +```http +PUT /api/user/profile +``` + +**请求参数** +```json +{ + "nickname": "新昵称", + "gender": "女", + "birthday": "1995-06-15" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "保存成功" +} +``` + +**Flutter 代码对应**: `profile_info_page.dart` - 保存按钮 + +--- + +#### 2.3 上传头像 + +```http +POST /api/user/avatar +Content-Type: multipart/form-data +``` + +**请求参数** +``` +file: (binary) +``` + +**响应** +```json +{ + "code": 0, + "data": { + "avatar_url": "https://oss.airhub.com/avatars/xxx.jpg" + } +} +``` + +**Flutter 代码对应**: `profile_info_page.dart` - `_pickImage()` + +--- + +### 3. 设备管理 + +#### 3.1 获取设备列表 + +```http +GET /api/devices +``` + +**响应** +```json +{ + "code": 0, + "data": { + "devices": [ + { + "id": "dev_001", + "name": "毛绒机芯", + "model": "Airhub_5G", + "status": "online", + "battery": 85, + "firmware_version": "2.1.3", + "is_ai": true, + "icon": "Capybara.png" + }, + { + "id": "dev_002", + "name": "电子吧唧 AI", + "model": "Badge_AI", + "status": "offline", + "battery": 0, + "is_ai": true + } + ] + } +} +``` + +**设备状态枚举**: +- `online` - 在线 +- `offline` - 离线 +- `pairing` - 配对中 +- `updating` - 固件更新中 + +**Flutter 代码对应**: `product_selection_page.dart` + +--- + +#### 3.2 获取设备详情 + +```http +GET /api/devices/{device_id} +``` + +**响应** +```json +{ + "code": 0, + "data": { + "id": "dev_001", + "name": "毛绒机芯", + "model": "Airhub_5G", + "status": "online", + "battery": 85, + "firmware_version": "2.1.3", + "mac_address": "AA:BB:CC:DD:EE:FF", + "settings": { + "nickname": "小毛球", + "user_name": "土豆", + "volume": 60, + "brightness": 85, + "allow_interrupt": true, + "privacy_mode": true + }, + "wifi_list": [ + { + "ssid": "Home_WiFi", + "is_connected": true + } + ], + "bound_memory_id": "mem_001" + } +} +``` + +**Flutter 代码对应**: `device_control_page.dart` - 设置页面 + +--- + +#### 3.3 更新设备设置 + +```http +PUT /api/devices/{device_id} +``` + +**请求参数** +```json +{ + "nickname": "新昵称", + "user_name": "主人称呼", + "volume": 70, + "brightness": 90, + "allow_interrupt": false, + "privacy_mode": true +} +``` + +**响应** +```json +{ + "code": 0, + "message": "设置已保存" +} +``` + +**PRD 来源**: `device-control.html` - 设备设置面板 + +--- + +#### 3.4 绑定设备 + +```http +POST /api/devices/{device_id}/bind +``` + +**请求参数** +```json +{ + "mac_address": "AA:BB:CC:DD:EE:FF", + "wifi_ssid": "Home_WiFi", + "wifi_password": "password123" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "设备绑定成功", + "data": { + "device_id": "dev_001", + "memory_id": "mem_001" + } +} +``` + +**PRD 来源**: `bluetooth.html`, `wifi-config.html` + +--- + +#### 3.5 解绑设备 + +```http +DELETE /api/devices/{device_id}/unbind +``` + +**响应** +```json +{ + "code": 0, + "message": "设备已解绑", + "data": { + "memory_id": "mem_001", + "memory_name": "Cloud_Mem_01" + } +} +``` + +**说明**: 解绑后角色记忆自动保存到云端 + +**PRD 来源**: `device-control.html` - 解绑设备弹窗 + +--- + +#### 3.6 配置设备 WiFi + +```http +POST /api/devices/{device_id}/wifi +``` + +**请求参数** +```json +{ + "ssid": "New_WiFi", + "password": "password123" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "WiFi 配置成功" +} +``` + +**PRD 来源**: `wifi-config.html` + +--- + +### 4. 角色记忆 + +#### 4.1 获取角色记忆列表 + +```http +GET /api/agents +``` + +**响应** +```json +{ + "code": 0, + "data": { + "agents": [ + { + "id": "mem_001", + "name": "Airhub_Mem_01", + "icon": "🧠", + "nickname": "小毛球", + "bound_device": { + "id": "dev_001", + "name": "Airhub_5G" + }, + "status": "bound", + "created_at": "2025-01-15" + }, + { + "id": "mem_002", + "name": "Airhub_Mem_02", + "icon": "🐾", + "nickname": "豆豆", + "bound_device": null, + "status": "unbound", + "created_at": "2024-08-22" + } + ] + } +} +``` + +**角色记忆状态**: +- `bound` - 已绑定设备 +- `unbound` - 未绑定 (可注入) + +**Flutter 代码对应**: `agent_manage_page.dart` + +--- + +#### 4.2 解绑角色记忆 + +```http +POST /api/agents/{agent_id}/unbind +``` + +**响应** +```json +{ + "code": 0, + "message": "已解绑角色记忆,数据已保留在云端" +} +``` + +--- + +#### 4.3 注入角色记忆 + +```http +POST /api/agents/{agent_id}/inject +``` + +**请求参数** +```json +{ + "device_id": "dev_002" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "角色记忆注入成功" +} +``` + +**PRD 来源**: `agent-manage.html` - 注入设备按钮 + +--- + +### 5. 故事模块 + +#### 5.1 获取故事列表 + +```http +GET /api/stories +``` + +**查询参数** +``` +shelf_id: 1 # 书架 ID,可选 +page: 1 # 页码 +page_size: 20 # 每页数量 +``` + +**响应** +```json +{ + "code": 0, + "data": { + "stories": [ + { + "id": "story_001", + "title": "卡皮巴拉的奇幻漂流", + "cover_url": "https://oss.airhub.com/covers/xxx.jpg", + "content": "在一条蜿蜒的小河边...", + "has_video": true, + "video_url": "https://oss.airhub.com/videos/xxx.mp4", + "created_at": "2025-01-20T15:30:00Z" + } + ], + "total": 5, + "page": 1, + "page_size": 20 + } +} +``` + +**Flutter 代码对应**: `device_control_page.dart` - `_mockStories` + +--- + +#### 5.2 获取故事详情 + +```http +GET /api/stories/{story_id} +``` + +**响应** +```json +{ + "code": 0, + "data": { + "id": "story_001", + "title": "卡皮巴拉的奇幻漂流", + "cover_url": "https://oss.airhub.com/covers/xxx.jpg", + "content": "完整故事内容...", + "has_video": true, + "video_url": "https://oss.airhub.com/videos/xxx.mp4", + "created_at": "2025-01-20T15:30:00Z" + } +} +``` + +**Flutter 代码对应**: `story_detail_page.dart` + +--- + +#### 5.3 生成故事 (SSE 流式) + +```http +POST /api/stories/generate +Accept: text/event-stream +``` + +**请求参数** +```json +{ + "mode": "random", + "prompt": "关于卡皮巴拉的冒险故事", + "theme": "adventure" +} +``` + +**生成模式**: +- `random` - 随机生成 +- `keyword` - 关键词生成 +- `theme` - 主题生成 + +**SSE 响应流** +``` +data: {"stage": "generating", "progress": 10, "message": "正在构思故事大纲..."} + +data: {"stage": "generating", "progress": 50, "message": "正在撰写故事内容..."} + +data: {"stage": "done", "progress": 100, "story": {...}} +``` + +**Flutter 代码对应**: `story_loading_page.dart` + +--- + +#### 5.4 保存故事 + +```http +POST /api/stories/{story_id}/save +``` + +**请求参数** +```json +{ + "shelf_id": 1 +} +``` + +**响应** +```json +{ + "code": 0, + "message": "故事已保存到书架" +} +``` + +**PRD 来源**: `story-detail.html` - 保存故事按钮 + +--- + +#### 5.5 生成动态绘本 (SSE 流式) + +```http +POST /api/stories/{story_id}/video +Accept: text/event-stream +``` + +**SSE 响应流** +``` +data: {"stage": "processing", "progress": 20, "message": "正在生成插画..."} + +data: {"stage": "processing", "progress": 60, "message": "正在合成视频..."} + +data: {"stage": "done", "progress": 100, "video_url": "https://..."} +``` + +**说明**: 生成动态绘本消耗 10 SP (积分) + +**Flutter 代码对应**: `story_detail_page.dart` - `runGenerationProcess()` + +--- + +#### 5.6 解锁新书架 + +```http +POST /api/stories/shelves/unlock +``` + +**请求参数** +```json +{ + "cost_points": 500 +} +``` + +**响应** +```json +{ + "code": 0, + "message": "解锁成功", + "data": { + "shelf_id": 2, + "remaining_points": 1500 + } +} +``` + +**Flutter 代码对应**: `device_control_page.dart` - `_showUnlockDialog()` + +--- + +### 6. 音乐模块 + +> ⚠️ **重要**: 此模块在 `music-creation.html` 中已有实际 API 调用实现 + +#### 6.1 生成音乐 (SSE 流式) + +```http +POST /api/create_music +Accept: text/event-stream +``` + +**请求参数** +```json +{ + "text": "水豚在雨中等公交,心情却很平静", + "mood": "chill" +} +``` + +**心情类型**: +- `chill` - Chill Lofi (慵懒·治愈) +- `happy` - Happy Funk (活力·奔跑) +- `sleep` - Deep Sleep (白噪音·助眠) +- `focus` - Focus Flow (心流·专注) +- `mystery` - 盲盒惊喜 (AI随机) +- `custom` - 自由创作 + +**SSE 响应流** +``` +data: {"stage": "connecting", "progress": 5, "message": "🎼 正在连接 AI..."} + +data: {"stage": "lyrics", "progress": 20, "message": "🎵 正在生成歌词..."} + +data: {"stage": "music", "progress": 30, "message": "🎹 正在作曲..."} + +data: {"stage": "done", "progress": 100, "file_path": "/music/xxx.mp3", "metadata": {"lyrics": "歌词内容..."}} +``` + +**错误响应** +``` +data: {"stage": "error", "message": "生成失败,请重试"} +``` + +**HTML 代码位置**: `music-creation.html:1730` + +**Flutter 代码对应**: `music_creation_page.dart` - `_mockGenerate()` + +--- + +#### 6.2 获取播放列表 + +```http +GET /api/playlist +``` + +**响应** +```json +{ + "code": 0, + "data": { + "playlist": [ + { + "id": 1, + "title": "卡皮巴拉蹦蹦蹦", + "lyrics": "卡皮巴拉\n啦啦啦啦...", + "audio_url": "/music/卡皮巴拉蹦蹦蹦.mp3", + "cover_url": "Capybara.png", + "mood": "happy", + "duration": 204, + "created_at": "2025-01-15T10:00:00Z" + } + ] + } +} +``` + +**HTML 代码位置**: `music-creation.html:2006` + +**Flutter 代码对应**: `music_creation_page.dart` - `_playlist` + +--- + +#### 6.3 删除播放列表项 + +```http +DELETE /api/playlist/{track_id} +``` + +**响应** +```json +{ + "code": 0, + "message": "已从播放列表移除" +} +``` + +--- + +#### 6.4 收藏音乐 + +```http +POST /api/playlist/{track_id}/favorite +``` + +**响应** +```json +{ + "code": 0, + "message": "已收藏" +} +``` + +--- + +### 7. 通知模块 + +#### 7.1 获取通知列表 + +```http +GET /api/notifications +``` + +**查询参数** +``` +type: all # all, system, activity, device +page: 1 +page_size: 20 +``` + +**响应** +```json +{ + "code": 0, + "data": { + "notifications": [ + { + "id": "notif_001", + "type": "system", + "title": "系统更新", + "description": "Airhub V1.2.0 版本更新已准备就绪", + "content": "

更新说明:

  • 新增喂养指南功能
", + "is_read": false, + "created_at": "2025-02-09T10:30:00Z" + }, + { + "id": "notif_002", + "type": "activity", + "title": "新春活动", + "description": "领取您的新春限定水豚皮肤", + "image_url": "https://...", + "is_read": true, + "created_at": "2025-02-08T09:00:00Z" + } + ], + "unread_count": 1 + } +} +``` + +**通知类型**: +- `system` - 系统通知 +- `activity` - 活动通知 +- `device` - 设备通知 + +**Flutter 代码对应**: `notification_page.dart` + +--- + +#### 7.2 标记通知已读 + +```http +PUT /api/notifications/{notification_id}/read +``` + +**响应** +```json +{ + "code": 0, + "message": "已标记为已读" +} +``` + +--- + +#### 7.3 标记全部已读 + +```http +PUT /api/notifications/read-all +``` + +**响应** +```json +{ + "code": 0, + "message": "全部标记为已读" +} +``` + +--- + +### 8. 系统接口 + +#### 8.1 提交意见反馈 + +```http +POST /api/feedback +``` + +**请求参数** +```json +{ + "content": "希望增加深色模式功能", + "contact": "user@email.com" +} +``` + +**响应** +```json +{ + "code": 0, + "message": "感谢您的反馈!" +} +``` + +**Flutter 代码对应**: `feedback_dialog.dart` + +--- + +#### 8.2 检查版本更新 + +```http +GET /api/version/check +``` + +**查询参数** +``` +platform: ios # ios, android +current_version: 1.0.0 +``` + +**响应** +```json +{ + "code": 0, + "data": { + "latest_version": "1.2.0", + "current_version": "1.0.0", + "update_required": false, + "force_update": false, + "update_url": "https://apps.apple.com/...", + "release_notes": "1. 新增喂养指南\n2. 优化连接稳定性" + } +} +``` + +**Flutter 代码对应**: `settings_page.dart` - 检查更新 + +--- + +## 数据模型 + +### User (用户) + +```typescript +interface User { + id: string; + phone: string; // 脱敏显示 + nickname: string; + avatar_url: string | null; + gender: '男' | '女' | null; + birthday: string | null; // YYYY-MM-DD + created_at: string; +} +``` + +### Device (设备) + +```typescript +interface Device { + id: string; + name: string; + model: string; + status: 'online' | 'offline' | 'pairing' | 'updating'; + battery: number; // 0-100 + firmware_version: string; + mac_address: string; + is_ai: boolean; + settings: DeviceSettings; + bound_memory_id: string | null; +} + +interface DeviceSettings { + nickname: string; // 设备昵称 + user_name: string; // 用户称呼 + volume: number; // 0-100 + brightness: number; // 0-100 + allow_interrupt: boolean; + privacy_mode: boolean; +} +``` + +### Agent (角色记忆) + +```typescript +interface Agent { + id: string; + name: string; // Airhub_Mem_01 + icon: string; // Emoji + nickname: string; // 小毛球 + bound_device: Device | null; + status: 'bound' | 'unbound'; + created_at: string; +} +``` + +### Story (故事) + +```typescript +interface Story { + id: string; + title: string; + cover_url: string; + content: string; + has_video: boolean; + video_url: string | null; + shelf_id: number; + created_at: string; +} +``` + +### Track (音乐) + +```typescript +interface Track { + id: number; + title: string; + lyrics: string; + audio_url: string; + cover_url: string; + mood: 'chill' | 'happy' | 'sleep' | 'focus' | 'mystery' | 'custom'; + duration: number; // 秒 + created_at: string; +} +``` + +### Notification (通知) + +```typescript +interface Notification { + id: string; + type: 'system' | 'activity' | 'device'; + title: string; + description: string; + content: string | null; // HTML 内容 + image_url: string | null; + is_read: boolean; + created_at: string; +} +``` + +--- + +## 开发优先级 + +| 优先级 | 模块 | 说明 | 依赖 | +|--------|------|------|------| +| **P0** | 用户认证 | 登录/注册是基础功能 | 运营商 SDK | +| **P0** | 设备管理 | 核心业务:设备绑定、配网 | 蓝牙 SDK | +| **P1** | 音乐模块 | 已有 HTML 实现,需对接 MiniMax API | MiniMax API | +| **P1** | 故事模块 | 核心功能:AI 生成故事 | LLM API | +| **P2** | 角色记忆 | 差异化功能 | 设备管理 | +| **P2** | 用户信息 | 个人中心相关 | 用户认证 | +| **P3** | 通知模块 | 辅助功能 | 推送 SDK | +| **P3** | 系统接口 | 反馈、版本检查 | - | + +--- + +## 附录:代码对照表 + +| 接口 | PRD HTML 文件 | Flutter 代码 | +|------|---------------|--------------| +| 登录 | `login.html` | `auth_remote_data_source.dart` | +| 用户资料 | `profile.html`, `profile-info.html` | `profile_page.dart`, `profile_info_page.dart` | +| 设备管理 | `products.html`, `device-control.html` | `product_selection_page.dart`, `device_control_page.dart` | +| 配网 | `bluetooth.html`, `wifi-config.html` | `bluetooth_page.dart`, `wifi_config_page.dart` | +| 角色记忆 | `agent-manage.html` | `agent_manage_page.dart` | +| 故事 | `story-detail.html`, `story-loading.html` | `story_detail_page.dart`, `story_loading_page.dart` | +| 音乐 | `music-creation.html` | `music_creation_page.dart` | +| 通知 | `notifications.html` | `notification_page.dart` | +| 设置 | `settings.html` | `settings_page.dart` | + +--- + +## 更新日志 + +| 版本 | 日期 | 说明 | +|------|------|------| +| 1.0.0 | 2025-02-09 | 初版,基于 PRD 和 Flutter 代码分析 | + +--- + +*本文档由 Claude Code 自动生成,如有问题请联系开发团队。* diff --git a/本地localhost运行.md b/本地localhost运行.md new file mode 100644 index 0000000..ff854a9 --- /dev/null +++ b/本地localhost运行.md @@ -0,0 +1,130 @@ +# Flutter Web 本地调试启动指南 + +> 本文档供 AI 编码助手阅读,用于在本项目中正确启动 Flutter Web 调试环境。 + +## 项目结构 + +- Flutter 应用目录:`airhub_app/` +- 后端服务入口:`server.py`(根目录,FastAPI + Uvicorn,端口 3000) +- 前端端口:`8080` + +## 环境要求 + +- Flutter SDK(3.x) +- Python 3.x(后端服务) +- PowerShell(Windows 环境) + +## 操作系统 + +Windows(所有命令均为 PowerShell 语法) + +--- + +## 启动流程(严格按顺序执行) + +### 1. 杀掉旧进程并确认端口空闲 + +```powershell +# 杀掉占用 8080 和 3000 的旧进程 +Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue | ForEach-Object { taskkill /F /PID $_.OwningProcess 2>$null } +Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue | ForEach-Object { taskkill /F /PID $_.OwningProcess 2>$null } + +# 等待端口释放 +Start-Sleep -Seconds 3 + +# 确认端口已空闲(无输出 = 空闲) +Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue +Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue +``` + +### 2. 启动后端服务器(音乐生成功能依赖此服务) + +```powershell +# 工作目录:项目根目录 +cd d:\Airhub +python server.py +``` + +成功标志: +``` +INFO: Uvicorn running on http://0.0.0.0:3000 (Press CTRL+C to quit) +[Server] Music Server running on http://localhost:3000 +``` + +### 3. 设置国内镜像源 + 启动 Flutter Web Server + +```powershell +# 工作目录:airhub_app 子目录 +cd d:\Airhub\airhub_app + +# 设置镜像源(必须,否则网络超时) +$env:PUB_HOSTED_URL = "https://pub.flutter-io.cn" +$env:FLUTTER_STORAGE_BASE_URL = "https://storage.flutter-io.cn" + +# 启动 web-server 模式 +flutter run -d web-server --web-port=8080 --no-pub +``` + +成功标志: +``` +lib\main.dart is being served at http://localhost:8080 +``` + +### 4. 访问应用 + +浏览器打开:`http://localhost:8080` + +--- + +## 关键规则 + +### 必须使用 `web-server` 模式 +- **禁止**使用 `flutter run -d chrome`(会弹出系统 Chrome 窗口,不可控) +- **必须**使用 `flutter run -d web-server`(只启动 HTTP 服务,手动用浏览器访问) + +### `--no-pub` 的使用条件 +- 仅修改 Dart 代码(无新依赖、无新 asset)→ 加 `--no-pub`,编译更快 +- 新增了 `pubspec.yaml` 依赖或 `assets/` 资源文件 → **不能**加 `--no-pub` + +### 端口管理 +- 固定使用 8080(Flutter)和 3000(后端),不要换端口绕过占用 +- 每次启动前必须先确认端口空闲 +- 停止服务后等 3 秒再重新启动 + +### 热重载 +- 在 Flutter 终端按 `r` = 热重载(保留页面状态) +- 按 `R` = 热重启(重置页面状态) +- 浏览器 `Ctrl+Shift+R` = 强制刷新 + +--- + +## 停止服务 + +```powershell +# 方法1:在 Flutter 终端按 q 退出 + +# 方法2:强制杀进程 +Get-NetTCPConnection -LocalPort 8080 | ForEach-Object { taskkill /F /PID $_.OwningProcess } +Get-NetTCPConnection -LocalPort 3000 | ForEach-Object { taskkill /F /PID $_.OwningProcess } +``` + +--- + +## 常见问题排查 + +| 问题 | 原因 | 解决方案 | +|------|------|---------| +| 端口被占用 | 旧进程未退出 | 执行第1步杀进程,等3秒 | +| 编译报错找不到包 | 使用了 `--no-pub` 但有新依赖 | 去掉 `--no-pub` 重新编译 | +| 网络超时 | 未设置镜像源 | 设置 `PUB_HOSTED_URL` 和 `FLUTTER_STORAGE_BASE_URL` | +| 页面白屏 | 缓存问题 | 浏览器 `Ctrl+Shift+R` 强刷 | +| 音乐功能不工作 | 后端未启动 | 先启动 `python server.py` | + +--- + +## 编译耗时参考 + +- 首次完整编译(含 pub get):90-120 秒 +- 增量编译(`--no-pub`):60-90 秒 +- 热重载(按 r):3-5 秒 +- 热重启(按 R):10-20 秒