merge: resolve conflict in notification_page (keep ClipRect fix)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
seaislee1209 2026-02-10 22:52:23 +08:00
commit f3ef1d1242
135 changed files with 13273 additions and 883 deletions

View File

@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(flutter doctor:*)",
"Bash(flutter devices:*)",
"Bash(flutter pub get:*)",
"Bash(pod install)",
"Bash(flutter run:*)"
]
}
}

View File

@ -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

View File

@ -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}"

View File

@ -5,6 +5,9 @@ targets:
options:
build_extensions:
'^lib/{{}}.dart': 'lib/{{}}.g.dart'
json_serializable:
options:
field_rename: snake
freezed:
options:
build_extensions:

View File

@ -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

View File

@ -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>

View 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;
}

View 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;
// 401token
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);
}
}

View 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';

View 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';
}

View 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();
}
}

View 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';

View File

@ -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',

View File

@ -48,4 +48,4 @@ final class GoRouterProvider
}
}
String _$goRouterHash() => r'937320fb6893b1da17afec22844ae01cf2e22441';
String _$goRouterHash() => r'8e620e452bb81f2c6ed87b136283a9e508dca2e9';

View 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);
}
});
// MTUiOS
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');
}
}
}

View 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 (_) {
//
}
}
}

View 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';

View File

@ -0,0 +1,103 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
import 'package:riverpod_annotation/riverpod_annotation.dart';
// Web 使 stubali_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;
},
);
}
}

View 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';

View 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;
}

View File

@ -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);
}
}

View File

@ -55,4 +55,4 @@ final class AuthRemoteDataSourceProvider
}
String _$authRemoteDataSourceHash() =>
r'b6a9edd1b6c48be8564688bac362316f598b4432';
r'9f874814620b5a8bcdf56417a68ed7cba404a9e9';

View File

@ -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));
}
}
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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

View File

@ -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};

View File

@ -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);

View File

@ -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?,
));
}

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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;
},
);
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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},
);
}
}

View File

@ -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';

View File

@ -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));
}
}
}

View File

@ -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';

View 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);
}

View File

@ -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

View 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,
};

View File

@ -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);
}

View File

@ -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

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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));
}
}
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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

View File

@ -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,
};

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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,
});
}
}

View File

@ -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';

View File

@ -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));
}
}
}

View File

@ -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';

View 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);
}

View File

@ -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

View 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,
};

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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&current_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>;
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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));
}
}
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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,
),
],
],

View File

@ -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(

View File

@ -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}' : '删除失败');
}
},
);
}
}

View File

@ -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> {
/// idnull
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;
}
}
}

View File

@ -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(

View File

@ -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),
],
),
),

View File

@ -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);
}
},
);
}

View File

@ -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);
}
}
}
},
);
}

View File

@ -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,

View File

@ -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
View File

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View 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
View 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

View 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

View 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 */;
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View 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
}
}

View File

@ -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