merge: resolve conflict in notification_page (keep ClipRect fix)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
f3ef1d1242
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(flutter doctor:*)",
|
||||
"Bash(flutter devices:*)",
|
||||
"Bash(flutter pub get:*)",
|
||||
"Bash(pod install)",
|
||||
"Bash(flutter run:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Bluetooth permissions -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:label="airhub_app"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@ -5,6 +5,9 @@ targets:
|
||||
options:
|
||||
build_extensions:
|
||||
'^lib/{{}}.dart': 'lib/{{}}.g.dart'
|
||||
json_serializable:
|
||||
options:
|
||||
field_rename: snake
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
@ -1,40 +1,79 @@
|
||||
PODS:
|
||||
- ali_auth (1.3.7):
|
||||
- Flutter
|
||||
- MJExtension
|
||||
- SDWebImage
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_blue_plus_darwin (0.0.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- MJExtension (3.4.2)
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.6):
|
||||
- SDWebImage/Core (= 5.21.6)
|
||||
- SDWebImage/Core (5.21.6)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- ali_auth (from `.symlinks/plugins/ali_auth/ios`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- MJExtension
|
||||
- SDWebImage
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
ali_auth:
|
||||
:path: ".symlinks/plugins/ali_auth/ios"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_blue_plus_darwin:
|
||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
ali_auth: fe9a6188a90eb39227f3674c05a71383ac4ec6a2
|
||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
||||
MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
@ -45,6 +45,12 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>需要蓝牙权限来搜索和连接您的设备</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要位置权限以扫描附近的蓝牙设备</string>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
|
||||
20
airhub_app/lib/core/errors/exceptions.dart
Normal file
20
airhub_app/lib/core/errors/exceptions.dart
Normal file
@ -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;
|
||||
}
|
||||
226
airhub_app/lib/core/network/api_client.dart
Normal file
226
airhub_app/lib/core/network/api_client.dart
Normal file
@ -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<dynamic> get(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final response = await _request(
|
||||
() => _dio.get(path, queryParameters: queryParameters),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// POST 请求,返回 data 字段
|
||||
Future<dynamic> post(
|
||||
String path, {
|
||||
dynamic data,
|
||||
}) async {
|
||||
final response = await _request(
|
||||
() => _dio.post(path, data: data),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// PUT 请求,返回 data 字段
|
||||
Future<dynamic> put(
|
||||
String path, {
|
||||
dynamic data,
|
||||
}) async {
|
||||
final response = await _request(
|
||||
() => _dio.put(path, data: data),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// DELETE 请求,返回 data 字段
|
||||
Future<dynamic> delete(
|
||||
String path, {
|
||||
dynamic data,
|
||||
}) async {
|
||||
final response = await _request(
|
||||
() => _dio.delete(path, data: data),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// 统一请求处理:解析后端 {code, message, data} 格式
|
||||
Future<dynamic> _request(Future<Response> Function() request) async {
|
||||
try {
|
||||
final response = await request();
|
||||
final body = response.data;
|
||||
|
||||
if (body is Map<String, dynamic>) {
|
||||
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<String, dynamic>) {
|
||||
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<String, dynamic> && body['code'] == 0) {
|
||||
final data = body['data'] as Map<String, dynamic>;
|
||||
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<String, dynamic>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
51
airhub_app/lib/core/network/api_client.g.dart
Normal file
51
airhub_app/lib/core/network/api_client.g.dart
Normal file
@ -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<ApiClient, ApiClient, ApiClient>
|
||||
with $Provider<ApiClient> {
|
||||
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<ApiClient> $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<ApiClient>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$apiClientHash() => r'03fa482085a0f74d1526b1a511e1b3c555269918';
|
||||
16
airhub_app/lib/core/network/api_config.dart
Normal file
16
airhub_app/lib/core/network/api_config.dart
Normal file
@ -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';
|
||||
}
|
||||
54
airhub_app/lib/core/network/token_manager.dart
Normal file
54
airhub_app/lib/core/network/token_manager.dart
Normal file
@ -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<SharedPreferences> get _preferences async {
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
return _prefs!;
|
||||
}
|
||||
|
||||
Future<void> saveTokens({
|
||||
required String access,
|
||||
required String refresh,
|
||||
}) async {
|
||||
final prefs = await _preferences;
|
||||
await prefs.setString(_keyAccessToken, access);
|
||||
await prefs.setString(_keyRefreshToken, refresh);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<String?> getAccessToken() async {
|
||||
final prefs = await _preferences;
|
||||
return prefs.getString(_keyAccessToken);
|
||||
}
|
||||
|
||||
Future<String?> getRefreshToken() async {
|
||||
final prefs = await _preferences;
|
||||
return prefs.getString(_keyRefreshToken);
|
||||
}
|
||||
|
||||
Future<bool> hasToken() async {
|
||||
final token = await getAccessToken();
|
||||
return token != null && token.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<void> clearTokens() async {
|
||||
final prefs = await _preferences;
|
||||
await prefs.remove(_keyAccessToken);
|
||||
await prefs.remove(_keyRefreshToken);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
51
airhub_app/lib/core/network/token_manager.g.dart
Normal file
51
airhub_app/lib/core/network/token_manager.g.dart
Normal file
@ -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<TokenManager, TokenManager, TokenManager>
|
||||
with $Provider<TokenManager> {
|
||||
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<TokenManager> $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<TokenManager>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$tokenManagerHash() => r'94bb9e39530e1d18331ea750bd4b6c5d4f16f1e9';
|
||||
@ -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<String, dynamic>?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/device-control',
|
||||
|
||||
@ -48,4 +48,4 @@ final class GoRouterProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$goRouterHash() => r'937320fb6893b1da17afec22844ae01cf2e22441';
|
||||
String _$goRouterHash() => r'8e620e452bb81f2c6ed87b136283a9e508dca2e9';
|
||||
|
||||
282
airhub_app/lib/core/services/ble_provisioning_service.dart
Normal file
282
airhub_app/lib/core/services/ble_provisioning_service.dart
Normal file
@ -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<List<ScannedWifi>>.broadcast();
|
||||
Stream<List<ScannedWifi>> get onWifiList => _wifiListController.stream;
|
||||
|
||||
/// 用于传递 WiFi 连接状态
|
||||
final _wifiStatusController = StreamController<WifiResult>.broadcast();
|
||||
Stream<WifiResult> get onWifiStatus => _wifiStatusController.stream;
|
||||
|
||||
/// 用于传递连接断开事件
|
||||
final _disconnectController = StreamController<void>.broadcast();
|
||||
Stream<void> get onDisconnect => _disconnectController.stream;
|
||||
|
||||
/// 临时存储 WiFi 列表条目
|
||||
List<ScannedWifi> _pendingWifiList = [];
|
||||
|
||||
/// 连接到 BLE 设备并发现配网服务
|
||||
Future<bool> 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<void> requestWifiScan() async {
|
||||
_pendingWifiList = [];
|
||||
await _write([_ProvCmd.getWifiList]);
|
||||
debugPrint('[BLE Prov] 已发送 WiFi 扫描命令');
|
||||
}
|
||||
|
||||
/// 发送 WiFi 凭证并触发连接
|
||||
Future<void> 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<void> 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<void> _write(List<int> data) async {
|
||||
if (_writeChar == null) {
|
||||
debugPrint('[BLE Prov] writeChar 未就绪');
|
||||
return;
|
||||
}
|
||||
await _writeChar!.write(data, withoutResponse: false);
|
||||
}
|
||||
|
||||
/// 处理 Notify 数据
|
||||
void _handleNotify(List<int> 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<int> 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<int> 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<int> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
142
airhub_app/lib/core/services/log_center_service.dart
Normal file
142
airhub_app/lib/core/services/log_center_service.dart
Normal file
@ -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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic> payload) {
|
||||
// Web 不支持 Isolate,用 compute / 直接 fire-and-forget
|
||||
if (kIsWeb) {
|
||||
_send(payload);
|
||||
} else {
|
||||
// 原生平台用 Isolate 完全不阻塞 UI 线程
|
||||
Isolate.run(() => _sendStatic(payload));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _send(Map<String, dynamic> payload) async {
|
||||
try {
|
||||
await _dio.post(_url, data: payload);
|
||||
} catch (_) {
|
||||
// 静默失败,不影响 App 运行
|
||||
}
|
||||
}
|
||||
|
||||
/// Isolate 内使用的静态方法(独立 Dio 实例)
|
||||
static Future<void> _sendStatic(Map<String, dynamic> 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 (_) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
}
|
||||
56
airhub_app/lib/core/services/log_center_service.g.dart
Normal file
56
airhub_app/lib/core/services/log_center_service.g.dart
Normal file
@ -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<LogCenterService> {
|
||||
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<LogCenterService> $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<LogCenterService>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$logCenterServiceHash() => r'd32eef012bfcebde414b77bfc69fa9ffda09eb5e';
|
||||
103
airhub_app/lib/core/services/phone_auth_service.dart
Normal file
103
airhub_app/lib/core/services/phone_auth_service.dart
Normal file
@ -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<void> 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<String?> 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<String?>();
|
||||
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
56
airhub_app/lib/core/services/phone_auth_service.g.dart
Normal file
56
airhub_app/lib/core/services/phone_auth_service.g.dart
Normal file
@ -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<PhoneAuthService> {
|
||||
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<PhoneAuthService> $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<PhoneAuthService>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$phoneAuthServiceHash() => r'b7a4a2481eef4a3ddd0ce529c879dc5319a3106b';
|
||||
14
airhub_app/lib/core/services/phone_auth_service_stub.dart
Normal file
14
airhub_app/lib/core/services/phone_auth_service_stub.dart
Normal file
@ -0,0 +1,14 @@
|
||||
/// Web 平台 stub — ali_auth 不支持 Web,提供空实现
|
||||
class AliAuth {
|
||||
static Future<void> initSdk(dynamic model) async {}
|
||||
static void loginListen({required Function(Map<String, dynamic>) onEvent}) {}
|
||||
}
|
||||
|
||||
class AliAuthModel {
|
||||
AliAuthModel(String androidSk, String iosSk,
|
||||
{bool isDebug = false, bool autoQuitPage = false, dynamic pageType});
|
||||
}
|
||||
|
||||
class PageType {
|
||||
static const fullPort = 0;
|
||||
}
|
||||
@ -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<User> loginWithPhone(String phoneNumber, String code);
|
||||
Future<User> oneClickLogin();
|
||||
Future<void> 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<void> logout(String refreshToken);
|
||||
Future<void> 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<User> 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<void> sendCode(String phone) async {
|
||||
await _apiClient.post('/auth/send-code/', data: {'phone': phone});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> 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<String, dynamic>);
|
||||
}
|
||||
|
||||
@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<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout(String refreshToken) async {
|
||||
await _apiClient.post('/auth/logout/', data: {'refresh': refreshToken});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAccount() async {
|
||||
await _apiClient.delete('/auth/account/');
|
||||
}
|
||||
|
||||
({User user, AuthTokens tokens, bool isNewUser}) _parseLoginResponse(
|
||||
Map<String, dynamic> data) {
|
||||
final user = User.fromJson(data['user'] as Map<String, dynamic>);
|
||||
// 后端返回字段名为 'token'(非 'tokens')
|
||||
final tokens = AuthTokens.fromJson(data['token'] as Map<String, dynamic>);
|
||||
final isNewUser = data['is_new_user'] as bool? ?? false;
|
||||
return (user: user, tokens: tokens, isNewUser: isNewUser);
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,4 +55,4 @@ final class AuthRemoteDataSourceProvider
|
||||
}
|
||||
|
||||
String _$authRemoteDataSourceHash() =>
|
||||
r'b6a9edd1b6c48be8564688bac362316f598b4432';
|
||||
r'9f874814620b5a8bcdf56417a68ed7cba404a9e9';
|
||||
|
||||
@ -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<User?>.broadcast();
|
||||
User? _currentUser;
|
||||
|
||||
AuthRepositoryImpl(this._remoteDataSource);
|
||||
AuthRepositoryImpl(this._remoteDataSource, this._tokenManager);
|
||||
|
||||
@override
|
||||
Stream<User?> get authStateChanges => Stream.value(null); // Mock stream
|
||||
User? get currentUser => _currentUser;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, User>> loginWithPhone(
|
||||
String phoneNumber,
|
||||
String code,
|
||||
) async {
|
||||
Stream<User?> get authStateChanges => _authStateController.stream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> 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<Either<Failure, User>> oneClickLogin() async {
|
||||
Future<Either<Failure, User>> 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<Either<Failure, User>> 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<Either<Failure, void>> 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<Either<Failure, void>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<String, dynamic> json) =>
|
||||
_$AuthTokensFromJson(json);
|
||||
}
|
||||
@ -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>(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<AuthTokens> get copyWith => _$AuthTokensCopyWithImpl<AuthTokens>(this as AuthTokens, _$identity);
|
||||
|
||||
/// Serializes this AuthTokens to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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
|
||||
@ -0,0 +1,15 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_tokens.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AuthTokens _$AuthTokensFromJson(Map<String, dynamic> json) => _AuthTokens(
|
||||
access: json['access'] as String,
|
||||
refresh: json['refresh'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthTokensToJson(_AuthTokens instance) =>
|
||||
<String, dynamic>{'access': instance.access, 'refresh': instance.refresh};
|
||||
@ -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<String, dynamic> json) => _$UserFromJson(json);
|
||||
|
||||
@ -15,7 +15,7 @@ T _$identity<T>(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<User> get copyWith => _$UserCopyWithImpl<User>(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 extends Object?>(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(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 extends Object?>(TResult? Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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<String, dynamic> 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<String, dynamic> 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?,
|
||||
));
|
||||
}
|
||||
|
||||
@ -7,15 +7,19 @@ part of 'user.dart';
|
||||
// **************************************************************************
|
||||
|
||||
_User _$UserFromJson(Map<String, dynamic> 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<String, dynamic> _$UserToJson(_User instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'phoneNumber': instance.phoneNumber,
|
||||
'phone': instance.phone,
|
||||
'nickname': instance.nickname,
|
||||
'avatarUrl': instance.avatarUrl,
|
||||
'avatar': instance.avatar,
|
||||
'gender': instance.gender,
|
||||
'birthday': instance.birthday,
|
||||
};
|
||||
|
||||
@ -3,8 +3,11 @@ import '../../../../core/errors/failures.dart';
|
||||
import '../entities/user.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<Either<Failure, User>> loginWithPhone(String phoneNumber, String code);
|
||||
Future<Either<Failure, User>> oneClickLogin();
|
||||
Future<Either<Failure, void>> sendCode(String phone);
|
||||
Future<Either<Failure, User>> codeLogin(String phone, String code);
|
||||
Future<Either<Failure, User>> tokenLogin(String token);
|
||||
Future<Either<Failure, void>> logout();
|
||||
Future<Either<Failure, void>> deleteAccount();
|
||||
Stream<User?> get authStateChanges;
|
||||
User? get currentUser;
|
||||
}
|
||||
|
||||
@ -10,23 +10,76 @@ class AuthController extends _$AuthController {
|
||||
// Initial state is void (idle)
|
||||
}
|
||||
|
||||
Future<void> loginWithPhone(String phoneNumber, String code) async {
|
||||
Future<void> 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<void> oneClickLogin() async {
|
||||
Future<bool> 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<bool> 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<void> 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<bool> 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ final class AuthControllerProvider
|
||||
AuthController create() => AuthController();
|
||||
}
|
||||
|
||||
String _$authControllerHash() => r'e7278df3deb6222da1e5e8b8b6ec921493441758';
|
||||
String _$authControllerHash() => r'3a290ddd5b4b091786d5020ecb57b7fb1d3a287a';
|
||||
|
||||
abstract class _$AuthController extends $AsyncNotifier<void> {
|
||||
FutureOr<void> build();
|
||||
|
||||
@ -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<LoginPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleListener(BuildContext context, AsyncValue<void> next) {
|
||||
void _handleListener(BuildContext context, AsyncValue<void>? prev, AsyncValue<void> 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<LoginPage> {
|
||||
}
|
||||
|
||||
// Logic Methods
|
||||
void _doOneClickLogin() {
|
||||
ref.read(authControllerProvider.notifier).oneClickLogin();
|
||||
Future<void> _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<LoginPage> {
|
||||
AppToast.show(context, message, isError: isError);
|
||||
}
|
||||
|
||||
void _sendCode() {
|
||||
Future<void> _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<void> _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<void> _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<LoginPage> {
|
||||
// Listen to Auth State
|
||||
ref.listen(
|
||||
authControllerProvider,
|
||||
(_, next) => _handleListener(context, next),
|
||||
(prev, next) => _handleListener(context, prev, next),
|
||||
);
|
||||
|
||||
final isLoading = ref.watch(authControllerProvider).isLoading;
|
||||
|
||||
@ -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<Map<String, dynamic>> queryByMac(String mac);
|
||||
|
||||
/// POST /devices/verify/
|
||||
Future<Map<String, dynamic>> verifyDevice(String sn);
|
||||
|
||||
/// POST /devices/bind/
|
||||
Future<UserDevice> bindDevice(String sn, {int? spiritId});
|
||||
|
||||
/// GET /devices/my_devices/
|
||||
Future<List<UserDevice>> getMyDevices();
|
||||
|
||||
/// GET /devices/{id}/detail/
|
||||
Future<DeviceDetail> getDeviceDetail(int userDeviceId);
|
||||
|
||||
/// DELETE /devices/{id}/unbind/
|
||||
Future<void> unbindDevice(int userDeviceId);
|
||||
|
||||
/// PUT /devices/{id}/update-spirit/
|
||||
Future<UserDevice> updateSpirit(int userDeviceId, int spiritId);
|
||||
|
||||
/// PUT /devices/{id}/settings/
|
||||
Future<void> updateSettings(int userDeviceId, Map<String, dynamic> settings);
|
||||
|
||||
/// POST /devices/{id}/wifi/
|
||||
Future<void> 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<Map<String, dynamic>> queryByMac(String mac) async {
|
||||
final data = await _apiClient.get(
|
||||
'/devices/query-by-mac/',
|
||||
queryParameters: {'mac': mac},
|
||||
);
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> verifyDevice(String sn) async {
|
||||
final data = await _apiClient.post('/devices/verify/', data: {'sn': sn});
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDevice> bindDevice(String sn, {int? spiritId}) async {
|
||||
final body = <String, dynamic>{'sn': sn};
|
||||
if (spiritId != null) body['spirit_id'] = spiritId;
|
||||
final data = await _apiClient.post('/devices/bind/', data: body);
|
||||
return UserDevice.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<UserDevice>> getMyDevices() async {
|
||||
final data = await _apiClient.get('/devices/my_devices/');
|
||||
final list = data as List<dynamic>;
|
||||
return list
|
||||
.map((e) => UserDevice.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DeviceDetail> getDeviceDetail(int userDeviceId) async {
|
||||
final data = await _apiClient.get('/devices/$userDeviceId/detail/');
|
||||
return DeviceDetail.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unbindDevice(int userDeviceId) async {
|
||||
await _apiClient.delete('/devices/$userDeviceId/unbind/');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserDevice> 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<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSettings(
|
||||
int userDeviceId,
|
||||
Map<String, dynamic> settings,
|
||||
) async {
|
||||
await _apiClient.put('/devices/$userDeviceId/settings/', data: settings);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> configWifi(int userDeviceId, String ssid) async {
|
||||
await _apiClient.post(
|
||||
'/devices/$userDeviceId/wifi/',
|
||||
data: {'ssid': ssid},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<DeviceRemoteDataSource> {
|
||||
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<DeviceRemoteDataSource> $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<DeviceRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$deviceRemoteDataSourceHash() =>
|
||||
r'cc457ef1f933b66a63014b7ebb123478077096c3';
|
||||
@ -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<Either<Failure, Map<String, dynamic>>> 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<Either<Failure, Map<String, dynamic>>> 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<Either<Failure, UserDevice>> 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<Either<Failure, List<UserDevice>>> 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<Either<Failure, DeviceDetail>> 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<Either<Failure, void>> 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<Either<Failure, UserDevice>> 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<Either<Failure, void>> updateSettings(
|
||||
int userDeviceId,
|
||||
Map<String, dynamic> 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<Either<Failure, void>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<DeviceRepository> {
|
||||
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<DeviceRepository> $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<DeviceRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$deviceRepositoryHash() => r'54eaa070bb4dfb0e34704d4525db2472f239450c';
|
||||
55
airhub_app/lib/features/device/domain/entities/device.dart
Normal file
55
airhub_app/lib/features/device/domain/entities/device.dart
Normal file
@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$UserDeviceFromJson(json);
|
||||
}
|
||||
@ -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>(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<DeviceType> get copyWith => _$DeviceTypeCopyWithImpl<DeviceType>(this as DeviceType, _$identity);
|
||||
|
||||
/// Serializes this DeviceType to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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<DeviceInfo> get copyWith => _$DeviceInfoCopyWithImpl<DeviceInfo>(this as DeviceInfo, _$identity);
|
||||
|
||||
/// Serializes this DeviceInfo to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceInfo&&(identical(other.id, id) || other.id == id)&&(identical(other.sn, sn) || other.sn == sn)&&(identical(other.deviceType, deviceType) || other.deviceType == deviceType)&&(identical(other.deviceTypeInfo, deviceTypeInfo) || other.deviceTypeInfo == deviceTypeInfo)&&(identical(other.macAddress, macAddress) || other.macAddress == macAddress)&&(identical(other.name, name) || other.name == name)&&(identical(other.status, status) || other.status == status)&&(identical(other.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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.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 extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo():
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.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 extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeviceInfo() when $default != null:
|
||||
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.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<String, dynamic> 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<String, dynamic> 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<UserDevice> get copyWith => _$UserDeviceCopyWithImpl<UserDevice>(this as UserDevice, _$identity);
|
||||
|
||||
/// Serializes this UserDevice to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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
|
||||
80
airhub_app/lib/features/device/domain/entities/device.g.dart
Normal file
80
airhub_app/lib/features/device/domain/entities/device.g.dart
Normal file
@ -0,0 +1,80 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_DeviceType _$DeviceTypeFromJson(Map<String, dynamic> 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<String, dynamic> _$DeviceTypeToJson(_DeviceType instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) => _DeviceInfo(
|
||||
id: (json['id'] as num).toInt(),
|
||||
sn: json['sn'] as String,
|
||||
deviceType: json['device_type'] == null
|
||||
? null
|
||||
: DeviceType.fromJson(json['device_type'] as Map<String, dynamic>),
|
||||
deviceTypeInfo: json['device_type_info'] == null
|
||||
? null
|
||||
: DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>),
|
||||
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<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) => _UserDevice(
|
||||
id: (json['id'] as num).toInt(),
|
||||
device: DeviceInfo.fromJson(json['device'] as Map<String, dynamic>),
|
||||
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<String, dynamic> _$UserDeviceToJson(_UserDevice instance) =>
|
||||
<String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@ -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<DeviceWifi> wifiList,
|
||||
Map<String, dynamic>? boundSpirit,
|
||||
}) = _DeviceDetail;
|
||||
|
||||
factory DeviceDetail.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$DeviceSettingsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class DeviceWifi with _$DeviceWifi {
|
||||
const factory DeviceWifi({
|
||||
required String ssid,
|
||||
@Default(false) bool isConnected,
|
||||
}) = _DeviceWifi;
|
||||
|
||||
factory DeviceWifi.fromJson(Map<String, dynamic> json) =>
|
||||
_$DeviceWifiFromJson(json);
|
||||
}
|
||||
@ -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>(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<DeviceWifi> get wifiList; Map<String, dynamic>? 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<DeviceDetail> get copyWith => _$DeviceDetailCopyWithImpl<DeviceDetail>(this as DeviceDetail, _$identity);
|
||||
|
||||
/// Serializes this DeviceDetail to a JSON map.
|
||||
Map<String, dynamic> 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<DeviceWifi> wifiList, Map<String, dynamic>? 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<DeviceWifi>,boundSpirit: freezed == boundSpirit ? _self.boundSpirit : boundSpirit // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
));
|
||||
}
|
||||
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List<DeviceWifi> wifiList, Map<String, dynamic>? 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 extends Object?>(TResult Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List<DeviceWifi> wifiList, Map<String, dynamic>? 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 extends Object?>(TResult? Function( int id, String sn, String name, String status, int battery, String firmwareVersion, String? macAddress, bool isAi, String? icon, DeviceSettings? settings, List<DeviceWifi> wifiList, Map<String, dynamic>? 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<DeviceWifi> wifiList = const [], final Map<String, dynamic>? boundSpirit}): _wifiList = wifiList,_boundSpirit = boundSpirit;
|
||||
factory _DeviceDetail.fromJson(Map<String, dynamic> 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<DeviceWifi> _wifiList;
|
||||
@override@JsonKey() List<DeviceWifi> get wifiList {
|
||||
if (_wifiList is EqualUnmodifiableListView) return _wifiList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_wifiList);
|
||||
}
|
||||
|
||||
final Map<String, dynamic>? _boundSpirit;
|
||||
@override Map<String, dynamic>? 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<String, dynamic> 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<DeviceWifi> wifiList, Map<String, dynamic>? 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<DeviceWifi>,boundSpirit: freezed == boundSpirit ? _self._boundSpirit : boundSpirit // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,
|
||||
));
|
||||
}
|
||||
|
||||
/// 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<DeviceSettings> get copyWith => _$DeviceSettingsCopyWithImpl<DeviceSettings>(this as DeviceSettings, _$identity);
|
||||
|
||||
/// Serializes this DeviceSettings to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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<DeviceWifi> get copyWith => _$DeviceWifiCopyWithImpl<DeviceWifi>(this as DeviceWifi, _$identity);
|
||||
|
||||
/// Serializes this DeviceWifi to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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
|
||||
@ -0,0 +1,76 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_DeviceDetail _$DeviceDetailFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
wifiList:
|
||||
(json['wifi_list'] as List<dynamic>?)
|
||||
?.map((e) => DeviceWifi.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
boundSpirit: json['bound_spirit'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DeviceDetailToJson(_DeviceDetail instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> 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<String, dynamic> _$DeviceSettingsToJson(_DeviceSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'nickname': instance.nickname,
|
||||
'user_name': instance.userName,
|
||||
'volume': instance.volume,
|
||||
'brightness': instance.brightness,
|
||||
'allow_interrupt': instance.allowInterrupt,
|
||||
'privacy_mode': instance.privacyMode,
|
||||
};
|
||||
|
||||
_DeviceWifi _$DeviceWifiFromJson(Map<String, dynamic> json) => _DeviceWifi(
|
||||
ssid: json['ssid'] as String,
|
||||
isConnected: json['is_connected'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DeviceWifiToJson(_DeviceWifi instance) =>
|
||||
<String, dynamic>{
|
||||
'ssid': instance.ssid,
|
||||
'is_connected': instance.isConnected,
|
||||
};
|
||||
@ -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<Either<Failure, Map<String, dynamic>>> queryByMac(String mac);
|
||||
Future<Either<Failure, Map<String, dynamic>>> verifyDevice(String sn);
|
||||
Future<Either<Failure, UserDevice>> bindDevice(String sn, {int? spiritId});
|
||||
Future<Either<Failure, List<UserDevice>>> getMyDevices();
|
||||
Future<Either<Failure, DeviceDetail>> getDeviceDetail(int userDeviceId);
|
||||
Future<Either<Failure, void>> unbindDevice(int userDeviceId);
|
||||
Future<Either<Failure, UserDevice>> updateSpirit(int userDeviceId, int spiritId);
|
||||
Future<Either<Failure, void>> updateSettings(int userDeviceId, Map<String, dynamic> settings);
|
||||
Future<Either<Failure, void>> configWifi(int userDeviceId, String ssid);
|
||||
}
|
||||
@ -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<List<UserDevice>> build() async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.getMyDevices();
|
||||
return result.fold(
|
||||
(failure) => <UserDevice>[],
|
||||
(devices) => devices,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> 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<bool> 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<DeviceDetail?> build(int userDeviceId) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.getDeviceDetail(userDeviceId);
|
||||
return result.fold(
|
||||
(failure) => null,
|
||||
(detail) => detail,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> updateSettings(Map<String, dynamic> settings) async {
|
||||
final repository = ref.read(deviceRepositoryProvider);
|
||||
final result = await repository.updateSettings(userDeviceId, settings);
|
||||
return result.fold(
|
||||
(failure) => false,
|
||||
(_) {
|
||||
ref.invalidateSelf();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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();
|
||||
}
|
||||
}
|
||||
@ -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<DeviceController, List<UserDevice>> {
|
||||
/// 管理用户设备列表
|
||||
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<List<UserDevice>> {
|
||||
FutureOr<List<UserDevice>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref as $Ref<AsyncValue<List<UserDevice>>, List<UserDevice>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<UserDevice>>, List<UserDevice>>,
|
||||
AsyncValue<List<UserDevice>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理单个设备详情
|
||||
|
||||
@ProviderFor(DeviceDetailController)
|
||||
const deviceDetailControllerProvider = DeviceDetailControllerFamily._();
|
||||
|
||||
/// 管理单个设备详情
|
||||
final class DeviceDetailControllerProvider
|
||||
extends $AsyncNotifierProvider<DeviceDetailController, DeviceDetail?> {
|
||||
/// 管理单个设备详情
|
||||
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?>,
|
||||
DeviceDetail?,
|
||||
FutureOr<DeviceDetail?>,
|
||||
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<DeviceDetail?> {
|
||||
late final _$args = ref.$arg as int;
|
||||
int get userDeviceId => _$args;
|
||||
|
||||
FutureOr<DeviceDetail?> build(int userDeviceId);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<AsyncValue<DeviceDetail?>, DeviceDetail?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<DeviceDetail?>, DeviceDetail?>,
|
||||
AsyncValue<DeviceDetail?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@ -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<AppNotification> items})>
|
||||
listNotifications({String? type, int page = 1, int pageSize = 20});
|
||||
|
||||
/// DELETE /notifications/{id}/
|
||||
Future<void> deleteNotification(int id);
|
||||
|
||||
/// POST /notifications/{id}/read/
|
||||
Future<void> markAsRead(int id);
|
||||
|
||||
/// POST /notifications/read-all/
|
||||
Future<int> 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<AppNotification> items})>
|
||||
listNotifications({String? type, int page = 1, int pageSize = 20}) async {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
};
|
||||
if (type != null) queryParams['type'] = type;
|
||||
|
||||
final data = await _apiClient.get(
|
||||
'/notifications/',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
final map = data as Map<String, dynamic>;
|
||||
final items = (map['items'] as List<dynamic>)
|
||||
.map((e) => AppNotification.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return (
|
||||
total: map['total'] as int,
|
||||
unreadCount: map['unread_count'] as int,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteNotification(int id) async {
|
||||
await _apiClient.delete('/notifications/$id/');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> markAsRead(int id) async {
|
||||
await _apiClient.post('/notifications/$id/read/');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> markAllAsRead() async {
|
||||
final data = await _apiClient.post('/notifications/read-all/');
|
||||
final map = data as Map<String, dynamic>;
|
||||
return map['count'] as int;
|
||||
}
|
||||
}
|
||||
@ -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<NotificationRemoteDataSource> {
|
||||
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<NotificationRemoteDataSource> $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<NotificationRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$notificationRemoteDataSourceHash() =>
|
||||
r'4e9f903c888936a1f5ff6367213f079547b47047';
|
||||
@ -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<Either<Failure, ({int total, int unreadCount, List<AppNotification> 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<Either<Failure, void>> 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<Either<Failure, void>> 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<Either<Failure, int>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<NotificationRepository> {
|
||||
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<NotificationRepository> $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<NotificationRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$notificationRepositoryHash() =>
|
||||
r'ecfecb73514b4e3713b54be327ce323b398df355';
|
||||
@ -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<String, dynamic> json) =>
|
||||
_$AppNotificationFromJson(json);
|
||||
}
|
||||
@ -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>(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<AppNotification> get copyWith => _$AppNotificationCopyWithImpl<AppNotification>(this as AppNotification, _$identity);
|
||||
|
||||
/// Serializes this AppNotification to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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
|
||||
@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_notification.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AppNotification _$AppNotificationFromJson(Map<String, dynamic> 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<String, dynamic> _$AppNotificationToJson(_AppNotification instance) =>
|
||||
<String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/app_notification.dart';
|
||||
|
||||
abstract class NotificationRepository {
|
||||
Future<Either<Failure, ({int total, int unreadCount, List<AppNotification> items})>>
|
||||
listNotifications({String? type, int page, int pageSize});
|
||||
Future<Either<Failure, void>> deleteNotification(int id);
|
||||
Future<Either<Failure, void>> markAsRead(int id);
|
||||
Future<Either<Failure, int>> markAllAsRead();
|
||||
}
|
||||
@ -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<List<AppNotification>> build() async {
|
||||
final repository = ref.read(notificationRepositoryProvider);
|
||||
final result = await repository.listNotifications();
|
||||
return result.fold(
|
||||
(failure) => <AppNotification>[],
|
||||
(data) {
|
||||
_unreadCount = data.unreadCount;
|
||||
return data.items;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> 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<bool> 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();
|
||||
}
|
||||
}
|
||||
@ -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<NotificationController, List<AppNotification>> {
|
||||
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<List<AppNotification>> {
|
||||
FutureOr<List<AppNotification>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<List<AppNotification>>, List<AppNotification>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<List<AppNotification>>,
|
||||
List<AppNotification>
|
||||
>,
|
||||
AsyncValue<List<AppNotification>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@ -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<List<Spirit>> listSpirits();
|
||||
Future<Spirit> createSpirit(Map<String, dynamic> data);
|
||||
Future<Spirit> getSpirit(int id);
|
||||
Future<Spirit> updateSpirit(int id, Map<String, dynamic> data);
|
||||
Future<void> deleteSpirit(int id);
|
||||
Future<void> unbindSpirit(int id);
|
||||
Future<void> 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<List<Spirit>> listSpirits() async {
|
||||
final data = await _apiClient.get('/spirits/');
|
||||
final list = data as List<dynamic>;
|
||||
return list
|
||||
.map((e) => Spirit.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Spirit> createSpirit(Map<String, dynamic> data) async {
|
||||
final result = await _apiClient.post('/spirits/', data: data);
|
||||
return Spirit.fromJson(result as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Spirit> getSpirit(int id) async {
|
||||
final data = await _apiClient.get('/spirits/$id/');
|
||||
return Spirit.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Spirit> updateSpirit(int id, Map<String, dynamic> data) async {
|
||||
final result = await _apiClient.put('/spirits/$id/', data: data);
|
||||
return Spirit.fromJson(result as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteSpirit(int id) async {
|
||||
await _apiClient.delete('/spirits/$id/');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unbindSpirit(int id) async {
|
||||
await _apiClient.post('/spirits/$id/unbind/');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> injectSpirit(int id, int userDeviceId) async {
|
||||
await _apiClient.post('/spirits/$id/inject/', data: {
|
||||
'user_device_id': userDeviceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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<SpiritRemoteDataSource> {
|
||||
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<SpiritRemoteDataSource> $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<SpiritRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$spiritRemoteDataSourceHash() =>
|
||||
r'd968cc481ea0216cb82b898a1ea926094f8ee8f4';
|
||||
@ -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<Either<Failure, List<Spirit>>> 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<Either<Failure, Spirit>> createSpirit({
|
||||
required String name,
|
||||
String? avatar,
|
||||
String? prompt,
|
||||
String? memory,
|
||||
String? voiceId,
|
||||
}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{'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<Either<Failure, Spirit>> 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<Either<Failure, Spirit>> updateSpirit(int id, Map<String, dynamic> 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<Either<Failure, void>> 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<Either<Failure, void>> 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<Either<Failure, void>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<SpiritRepository> {
|
||||
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<SpiritRepository> $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<SpiritRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$spiritRepositoryHash() => r'c8ba175770cb9a4aa04b60abac3276ac87f17bc1';
|
||||
21
airhub_app/lib/features/spirit/domain/entities/spirit.dart
Normal file
21
airhub_app/lib/features/spirit/domain/entities/spirit.dart
Normal file
@ -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<String, dynamic> json) => _$SpiritFromJson(json);
|
||||
}
|
||||
@ -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>(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<Spirit> get copyWith => _$SpiritCopyWithImpl<Spirit>(this as Spirit, _$identity);
|
||||
|
||||
/// Serializes this Spirit to a JSON map.
|
||||
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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
|
||||
31
airhub_app/lib/features/spirit/domain/entities/spirit.g.dart
Normal file
31
airhub_app/lib/features/spirit/domain/entities/spirit.g.dart
Normal file
@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'spirit.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Spirit _$SpiritFromJson(Map<String, dynamic> 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<String, dynamic> _$SpiritToJson(_Spirit instance) => <String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/spirit.dart';
|
||||
|
||||
abstract class SpiritRepository {
|
||||
Future<Either<Failure, List<Spirit>>> listSpirits();
|
||||
Future<Either<Failure, Spirit>> createSpirit({
|
||||
required String name,
|
||||
String? avatar,
|
||||
String? prompt,
|
||||
String? memory,
|
||||
String? voiceId,
|
||||
});
|
||||
Future<Either<Failure, Spirit>> getSpirit(int id);
|
||||
Future<Either<Failure, Spirit>> updateSpirit(int id, Map<String, dynamic> data);
|
||||
Future<Either<Failure, void>> deleteSpirit(int id);
|
||||
Future<Either<Failure, void>> unbindSpirit(int id);
|
||||
Future<Either<Failure, void>> injectSpirit(int id, int userDeviceId);
|
||||
}
|
||||
@ -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<List<Spirit>> build() async {
|
||||
final repository = ref.read(spiritRepositoryProvider);
|
||||
final result = await repository.listSpirits();
|
||||
return result.fold(
|
||||
(failure) => <Spirit>[],
|
||||
(spirits) => spirits,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> 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<bool> 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<bool> 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();
|
||||
}
|
||||
}
|
||||
@ -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<SpiritController, List<Spirit>> {
|
||||
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<List<Spirit>> {
|
||||
FutureOr<List<Spirit>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Spirit>>, List<Spirit>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Spirit>>, List<Spirit>>,
|
||||
AsyncValue<List<Spirit>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@ -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<Map<String, dynamic>> submitFeedback(String content, {String? contact});
|
||||
|
||||
/// GET /version/check/?platform=xxx¤t_version=xxx
|
||||
Future<Map<String, dynamic>> 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<Map<String, dynamic>> submitFeedback(
|
||||
String content, {
|
||||
String? contact,
|
||||
}) async {
|
||||
final body = <String, dynamic>{'content': content};
|
||||
if (contact != null && contact.isNotEmpty) body['contact'] = contact;
|
||||
final data = await _apiClient.post('/feedback/', data: body);
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> checkVersion(
|
||||
String platform,
|
||||
String currentVersion,
|
||||
) async {
|
||||
final data = await _apiClient.get(
|
||||
'/version/check/',
|
||||
queryParameters: {
|
||||
'platform': platform,
|
||||
'current_version': currentVersion,
|
||||
},
|
||||
);
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
@ -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<SystemRemoteDataSource> {
|
||||
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<SystemRemoteDataSource> $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<SystemRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$systemRemoteDataSourceHash() =>
|
||||
r'ada09ecf278e031e82b96b36b847d4977356d4aa';
|
||||
@ -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<User> getMe();
|
||||
Future<User> updateMe(Map<String, dynamic> data);
|
||||
Future<String> 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<User> getMe() async {
|
||||
final data = await _apiClient.get('/users/me/');
|
||||
return User.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> updateMe(Map<String, dynamic> data) async {
|
||||
final result = await _apiClient.put('/users/update_me/', data: data);
|
||||
return User.fromJson(result as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> 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<String, dynamic>)['avatar_url'] as String;
|
||||
}
|
||||
}
|
||||
@ -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<UserRemoteDataSource> {
|
||||
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<UserRemoteDataSource> $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<UserRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userRemoteDataSourceHash() =>
|
||||
r'61338314bdae7e01a494e565c89fd02ab8d731b7';
|
||||
@ -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<Either<Failure, User>> 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<Either<Failure, User>> updateMe({
|
||||
String? nickname,
|
||||
String? gender,
|
||||
String? birthday,
|
||||
}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{};
|
||||
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<Either<Failure, String>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<UserRepository, UserRepository, UserRepository>
|
||||
with $Provider<UserRepository> {
|
||||
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<UserRepository> $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<UserRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userRepositoryHash() => r'bcdf0718d6e048bec2e3321db1595c5263baa8d2';
|
||||
@ -0,0 +1,13 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../auth/domain/entities/user.dart';
|
||||
|
||||
abstract class UserRepository {
|
||||
Future<Either<Failure, User>> getMe();
|
||||
Future<Either<Failure, User>> updateMe({
|
||||
String? nickname,
|
||||
String? gender,
|
||||
String? birthday,
|
||||
});
|
||||
Future<Either<Failure, String>> uploadAvatar(String filePath);
|
||||
}
|
||||
@ -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<User?> build() async {
|
||||
final repository = ref.read(userRepositoryProvider);
|
||||
final result = await repository.getMe();
|
||||
return result.fold(
|
||||
(failure) => null,
|
||||
(user) => user,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> 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();
|
||||
}
|
||||
}
|
||||
@ -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<UserController, User?> {
|
||||
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<User?> {
|
||||
FutureOr<User?> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<User?>, User?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<User?>, User?>,
|
||||
AsyncValue<User?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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<BluetoothPage> createState() => _BluetoothPageState();
|
||||
ConsumerState<BluetoothPage> createState() => _BluetoothPageState();
|
||||
}
|
||||
|
||||
class _BluetoothPageState extends State<BluetoothPage>
|
||||
class _BluetoothPageState extends ConsumerState<BluetoothPage>
|
||||
with TickerProviderStateMixin {
|
||||
/// Airhub 设备名前缀(硬件广播格式: Airhub_ + MAC)
|
||||
static const _airhubPrefix = 'Airhub_';
|
||||
|
||||
// 状态
|
||||
bool _isSearching = true;
|
||||
bool _isBluetoothOn = false;
|
||||
List<MockDevice> _devices = [];
|
||||
int _currentIndex = 0;
|
||||
|
||||
// 已查询过的 MAC → 设备信息缓存(避免重复调 API)
|
||||
final Map<String, Map<String, dynamic>> _macInfoCache = {};
|
||||
|
||||
// 动画控制器
|
||||
late AnimationController _searchAnimController;
|
||||
|
||||
// 滚轮控制器
|
||||
late FixedExtentScrollController _wheelController;
|
||||
|
||||
// 模拟设备数据
|
||||
static const List<MockDevice> _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<BluetoothAdapterState>? _bluetoothSubscription;
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
|
||||
// 是否已弹过蓝牙关闭提示(避免重复弹窗)
|
||||
bool _hasShownBluetoothDialog = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -111,61 +109,315 @@ class _BluetoothPageState extends State<BluetoothPage>
|
||||
// 滚轮控制器
|
||||
_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<String, BluetoothDevice> _pendingBleDevices = {};
|
||||
|
||||
/// 开始 BLE 扫描(持续扫描,直到找到设备并完成 API 查询)
|
||||
Future<void> _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<void> _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<void> _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<void> _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<BluetoothPage>
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@ -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<DeviceControlPage> createState() => _DeviceControlPageState();
|
||||
ConsumerState<DeviceControlPage> createState() => _DeviceControlPageState();
|
||||
}
|
||||
|
||||
class _DeviceControlPageState extends State<DeviceControlPage>
|
||||
class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int _currentIndex = 0; // 0: Home, 1: Story, 2: Music, 3: User
|
||||
|
||||
@ -216,71 +218,8 @@ class _DeviceControlPageState extends State<DeviceControlPage>
|
||||
// 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<DeviceControlPage>
|
||||
);
|
||||
}
|
||||
|
||||
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<DeviceControlPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlaceholderView(String title) {
|
||||
return Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavBar() {
|
||||
return Center(
|
||||
child: ClipRRect(
|
||||
|
||||
@ -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<AgentManagePage> createState() => _AgentManagePageState();
|
||||
ConsumerState<AgentManagePage> createState() => _AgentManagePageState();
|
||||
}
|
||||
|
||||
class _AgentManagePageState extends State<AgentManagePage> {
|
||||
// Mock data matching HTML
|
||||
final List<Map<String, String>> _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<AgentManagePage> {
|
||||
@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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAgentCard(Map<String, String> 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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
],
|
||||
),
|
||||
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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
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<AgentManagePage> {
|
||||
);
|
||||
}
|
||||
|
||||
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}' : '删除失败');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<NotificationPage> createState() => _NotificationPageState();
|
||||
ConsumerState<NotificationPage> createState() => _NotificationPageState();
|
||||
}
|
||||
|
||||
class _NotificationPageState extends State<NotificationPage> {
|
||||
/// 当前展开的通知 index(-1 表示全部折叠,手风琴模式)
|
||||
int _expandedIndex = -1;
|
||||
class _NotificationPageState extends ConsumerState<NotificationPage> {
|
||||
/// 当前展开的通知 id(null 表示全部折叠)
|
||||
int? _expandedId;
|
||||
|
||||
/// 已读标记(index set)
|
||||
final Set<int> _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<NotificationPage> {
|
||||
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<NotificationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Header ───
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
@ -141,13 +144,12 @@ class _NotificationPageState extends State<NotificationPage> {
|
||||
),
|
||||
),
|
||||
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<NotificationPage> {
|
||||
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<NotificationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 通知列表 ───
|
||||
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<NotificationPage> {
|
||||
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<NotificationPage> {
|
||||
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<NotificationPage> {
|
||||
),
|
||||
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<NotificationPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 箭头 + 未读红点
|
||||
Column(
|
||||
children: [
|
||||
AnimatedRotation(
|
||||
@ -342,57 +325,71 @@ class _NotificationPageState extends State<NotificationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 通知图标 ───
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ProfileInfoPage> createState() => _ProfileInfoPageState();
|
||||
ConsumerState<ProfileInfoPage> createState() => _ProfileInfoPageState();
|
||||
}
|
||||
|
||||
class _ProfileInfoPageState extends State<ProfileInfoPage> {
|
||||
String _gender = '男';
|
||||
String _birthday = '1994-12-09';
|
||||
File? _avatarImage;
|
||||
final TextEditingController _nicknameController = TextEditingController(
|
||||
text: '土豆',
|
||||
);
|
||||
class _ProfileInfoPageState extends ConsumerState<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
|
||||
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<ProfileInfoPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<ProfileInfoPage> {
|
||||
),
|
||||
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<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle bar
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
@ -359,13 +429,10 @@ class _ProfileInfoPageState extends State<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
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<ProfileInfoPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// iOS-style wheel date picker
|
||||
void _showBirthdayInput() {
|
||||
DateTime tempDate = DateTime.tryParse(_birthday) ?? DateTime(1994, 12, 9);
|
||||
|
||||
@ -473,7 +539,6 @@ class _ProfileInfoPageState extends State<ProfileInfoPage> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
decoration: const BoxDecoration(
|
||||
@ -527,7 +592,6 @@ class _ProfileInfoPageState extends State<ProfileInfoPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Cupertino date picker wheel
|
||||
Expanded(
|
||||
child: CupertinoTheme(
|
||||
data: const CupertinoThemeData(
|
||||
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<SettingsPage> createState() => _SettingsPageState();
|
||||
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
bool _notificationEnabled = true;
|
||||
|
||||
@override
|
||||
@ -70,8 +75,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_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<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<SettingsPage> {
|
||||
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<SettingsPage> {
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<SettingsPage> createState() => _SettingsPageState();
|
||||
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
// State for mock data
|
||||
String _deviceName = '小毛球';
|
||||
String _userName = '土豆';
|
||||
double _volume = 60;
|
||||
double _brightness = 85;
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
// 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<SettingsPage> {
|
||||
'修改设备昵称',
|
||||
_deviceName,
|
||||
(val) => setState(() => _deviceName = val),
|
||||
settingsKey: 'nickname',
|
||||
),
|
||||
),
|
||||
_buildDivider(),
|
||||
@ -106,6 +138,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
'修改你的称呼',
|
||||
_userName,
|
||||
(val) => setState(() => _userName = val),
|
||||
settingsKey: 'user_name',
|
||||
),
|
||||
),
|
||||
]),
|
||||
@ -117,7 +150,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_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<SettingsPage> {
|
||||
_brightness,
|
||||
'☀',
|
||||
'☼',
|
||||
(val) => setState(() => _brightness = val),
|
||||
(val) {
|
||||
setState(() => _brightness = val);
|
||||
},
|
||||
onChangeEnd: (val) => _saveSettings({'brightness': val.toInt()}),
|
||||
),
|
||||
]),
|
||||
|
||||
@ -152,10 +191,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_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<SettingsPage> {
|
||||
double value,
|
||||
String iconL,
|
||||
String iconR,
|
||||
ValueChanged<double> onChanged,
|
||||
) {
|
||||
ValueChanged<double> onChanged, {
|
||||
ValueChanged<double>? onChangeEnd,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@ -353,6 +403,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
min: 0,
|
||||
max: 100,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -376,11 +427,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveSettings(Map<String, dynamic> 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<String> onSaved,
|
||||
) {
|
||||
ValueSetter<String> onSaved, {
|
||||
String? settingsKey,
|
||||
}) {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
showGlassDialog(
|
||||
context: context,
|
||||
@ -394,6 +457,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
onConfirm: () {
|
||||
onSaved(controller.text);
|
||||
if (settingsKey != null) {
|
||||
_saveSettings({settingsKey: controller.text});
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
@ -417,15 +483,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<String, dynamic>? extra;
|
||||
|
||||
const WifiConfigPage({super.key, this.extra});
|
||||
|
||||
@override
|
||||
State<WifiConfigPage> createState() => _WifiConfigPageState();
|
||||
ConsumerState<WifiConfigPage> createState() => _WifiConfigPageState();
|
||||
}
|
||||
|
||||
class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentStep = 1;
|
||||
String _selectedWifiSsid = '';
|
||||
@ -23,36 +28,112 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
// 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<String, dynamic> _deviceInfo = {};
|
||||
|
||||
// Mock WiFi List
|
||||
final List<Map<String, dynamic>> _wifiList = [
|
||||
{'ssid': 'Home_5G', 'level': 4},
|
||||
{'ssid': 'Office_WiFi', 'level': 3},
|
||||
{'ssid': 'Guest_Network', 'level': 2},
|
||||
];
|
||||
// BLE Provisioning
|
||||
BleProvisioningService? _provService;
|
||||
List<ScannedWifi> _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<String, dynamic>) {
|
||||
_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<void> _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<void> _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<WifiConfigPage>
|
||||
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<void> _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<WifiConfigPage>
|
||||
} else {
|
||||
timer.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentStep = 4;
|
||||
});
|
||||
setState(() => _currentStep = 4);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -112,34 +236,24 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
);
|
||||
}
|
||||
|
||||
// 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<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48), // Balance back button
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -209,10 +317,10 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
}
|
||||
}
|
||||
|
||||
// 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<WifiConfigPage>
|
||||
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<String, dynamic> 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<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Enter Password
|
||||
// Step 2: 输入密码
|
||||
Widget _buildStep2() {
|
||||
return Column(
|
||||
children: [
|
||||
@ -352,16 +502,11 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
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<WifiConfigPage>
|
||||
size: 22,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
setState(() => _obscurePassword = !_obscurePassword);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -397,11 +540,10 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
);
|
||||
}
|
||||
|
||||
// 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<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Progress Bar
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: SizedBox(
|
||||
@ -438,7 +577,9 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
}
|
||||
}
|
||||
|
||||
// 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<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
// Check badge
|
||||
Positioned(
|
||||
bottom: -5,
|
||||
right: -5,
|
||||
@ -531,13 +686,9 @@ class _WifiConfigPageState extends State<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
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<WifiConfigPage>
|
||||
}
|
||||
|
||||
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<WifiConfigPage>
|
||||
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<WifiConfigPage>
|
||||
),
|
||||
),
|
||||
if (_currentStep < 4) const SizedBox(width: 16),
|
||||
|
||||
// Constrained button (not full-width)
|
||||
GradientButton(
|
||||
text: nextText,
|
||||
onPressed: _handleNext,
|
||||
|
||||
@ -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<FeedbackDialog> createState() => _FeedbackDialogState();
|
||||
}
|
||||
|
||||
class _FeedbackDialogState extends ConsumerState<FeedbackDialog> {
|
||||
final _contentController = TextEditingController();
|
||||
final _contactController = TextEditingController();
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_contactController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
7
airhub_app/macos/.gitignore
vendored
Normal file
7
airhub_app/macos/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
2
airhub_app/macos/Flutter/Flutter-Debug.xcconfig
Normal file
2
airhub_app/macos/Flutter/Flutter-Debug.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
2
airhub_app/macos/Flutter/Flutter-Release.xcconfig
Normal file
2
airhub_app/macos/Flutter/Flutter-Release.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
22
airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
22
airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
@ -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"))
|
||||
}
|
||||
42
airhub_app/macos/Podfile
Normal file
42
airhub_app/macos/Podfile
Normal file
@ -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
|
||||
56
airhub_app/macos/Podfile.lock
Normal file
56
airhub_app/macos/Podfile.lock
Normal file
@ -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
|
||||
801
airhub_app/macos/Runner.xcodeproj/project.pbxproj
Normal file
801
airhub_app/macos/Runner.xcodeproj/project.pbxproj
Normal file
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
6597D0BE9D7B206A2E3DE1AE /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* airhub_app.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
215332CEB40E691199867340 /* Pods_Runner.framework */,
|
||||
82E99DAA670872DCBD38DA9F /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "airhub_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "airhub_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "airhub_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "airhub_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
airhub_app/macos/Runner/AppDelegate.swift
Normal file
13
airhub_app/macos/Runner/AppDelegate.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user