Compare commits

..

No commits in common. "134da153d5ad75082e7e0bae0a84702a87896a8d" and "51164ae21a29cab712844b634897fef2bc6d95c2" have entirely different histories.

34 changed files with 540 additions and 3405 deletions

File diff suppressed because one or more lines are too long

View File

@ -23,9 +23,6 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- webview_flutter_wkwebview (0.0.1): - webview_flutter_wkwebview (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -39,7 +36,6 @@ DEPENDENCIES:
- just_audio (from `.symlinks/plugins/just_audio/darwin`) - just_audio (from `.symlinks/plugins/just_audio/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS: SPEC REPOS:
@ -64,8 +60,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
webview_flutter_wkwebview: webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
@ -80,7 +74,6 @@ SPEC CHECKSUMS:
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@ -26,10 +26,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>需要相机权限来拍照传图到徽章设备</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限来选择图片传图到徽章设备</string>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string> <string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>

View File

@ -143,6 +143,7 @@ class _AuthInterceptor extends Interceptor {
'/auth/phone-login/', '/auth/phone-login/',
'/auth/refresh/', '/auth/refresh/',
'/version/check/', '/version/check/',
'/devices/query-by-mac/',
]; ];
final needsAuth = !noAuthPaths.any((p) => options.path.contains(p)); final needsAuth = !noAuthPaths.any((p) => options.path.contains(p));

View File

@ -1,14 +1,6 @@
import 'package:flutter/foundation.dart' show kIsWeb;
class ApiConfig { class ApiConfig {
/// Web ///
static const String _localUrl = 'http://192.168.124.8:8000'; static const String baseUrl = 'http://192.168.124.8:8000';
/// 线APP
static const String _prodUrl = 'https://qiyuan-rtc-api.airlabs.art';
/// Web APP 线
static String get baseUrl => kIsWeb ? _localUrl : _prodUrl;
/// HTTPS /// HTTPS
static const String authBaseUrl = 'https://qiyuan-rtc-api.airlabs.art'; static const String authBaseUrl = 'https://qiyuan-rtc-api.airlabs.art';

View File

@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -11,23 +9,12 @@ import '../../pages/product_selection_page.dart';
import '../../pages/profile/profile_page.dart'; import '../../pages/profile/profile_page.dart';
import '../../pages/webview_page.dart'; import '../../pages/webview_page.dart';
import '../../pages/wifi_config_page.dart'; import '../../pages/wifi_config_page.dart';
import '../../features/badge/presentation/pages/badge_basic_control_page.dart';
import '../../features/badge/presentation/pages/badge_control_page.dart';
import '../../features/badge/presentation/pages/badge_home_page.dart'; import '../../features/badge/presentation/pages/badge_home_page.dart';
import '../../features/badge/presentation/pages/badge_transfer_page.dart'; import '../../features/badge/presentation/pages/badge_transfer_page.dart';
import '../../features/device/data/datasources/device_remote_data_source.dart';
import '../../theme/product_theme.dart';
import '../network/token_manager.dart'; import '../network/token_manager.dart';
part 'app_router.g.dart'; part 'app_router.g.dart';
/// + ProductType
const _productCodeRoutes = {
'KPBL-ON': (route: '/device-control', type: ProductType.capybara),
'DZBJ-ON': (route: '/badge-control', type: ProductType.badgeAi),
'DZBJ-OFF': (route: '/badge-basic-control', type: ProductType.badgeBasic),
};
@riverpod @riverpod
GoRouter goRouter(Ref ref) { GoRouter goRouter(Ref ref) {
final tokenManager = ref.watch(tokenManagerProvider); final tokenManager = ref.watch(tokenManagerProvider);
@ -43,37 +30,6 @@ GoRouter goRouter(Ref ref) {
return '/login'; return '/login';
} }
if (hasToken && isLoginRoute) { if (hasToken && isLoginRoute) {
// 使
try {
final dataSource = ref.read(deviceRemoteDataSourceProvider);
final devices = await dataSource.getMyDevices();
debugPrint('[Router] 已绑定设备数: ${devices.length}');
if (devices.isNotEmpty) {
// last_online_at 使
devices.sort((a, b) {
final ta = a.device.lastOnlineAt ?? '';
final tb = b.device.lastOnlineAt ?? '';
return tb.compareTo(ta);
});
final recent = devices.first;
final dt = recent.device.deviceType;
final dti = recent.device.deviceTypeInfo;
debugPrint('[Router] 最近设备 sn=${recent.device.sn}');
debugPrint('[Router] deviceType=$dt');
debugPrint('[Router] deviceTypeInfo=$dti');
final resolvedDt = dt ?? dti;
final code = resolvedDt?.productCode ?? '';
debugPrint('[Router] productCode=$code');
final mapping = _productCodeRoutes[code];
debugPrint('[Router] mapping=$mapping → route=${mapping?.route}');
if (mapping != null) {
ref.read(currentProductTypeProvider.notifier).set(mapping.type);
return mapping.route;
}
}
} catch (e) {
debugPrint('[Router] 获取设备失败: $e');
}
return '/home'; return '/home';
} }
return null; return null;
@ -107,14 +63,6 @@ GoRouter goRouter(Ref ref) {
path: '/webview_fallback', path: '/webview_fallback',
builder: (context, state) => const WebViewPage(), builder: (context, state) => const WebViewPage(),
), ),
GoRoute(
path: '/badge-control',
builder: (context, state) => const BadgeControlPage(),
),
GoRoute(
path: '/badge-basic-control',
builder: (context, state) => const BadgeBasicControlPage(),
),
GoRoute( GoRoute(
path: '/badge', path: '/badge',
builder: (context, state) => const BadgeHomePage(), builder: (context, state) => const BadgeHomePage(),
@ -125,7 +73,6 @@ GoRouter goRouter(Ref ref) {
final extra = state.extra as Map<String, dynamic>? ?? {}; final extra = state.extra as Map<String, dynamic>? ?? {};
return BadgeTransferPage( return BadgeTransferPage(
imageUrl: extra['imageUrl'] as String? ?? '', imageUrl: extra['imageUrl'] as String? ?? '',
imageBytes: extra['imageBytes'] as Uint8List?,
); );
}, },
), ),

View File

@ -48,4 +48,4 @@ final class GoRouterProvider
} }
} }
String _$goRouterHash() => r'276ed56c903c6bd5bb43569f7b8f58103d73198c'; String _$goRouterHash() => r'4b725bca8dba655c6abb2f19069fca97c8bebbb6';

View File

@ -219,7 +219,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final success = await ref.read(authControllerProvider.notifier).tokenLogin(token); final success = await ref.read(authControllerProvider.notifier).tokenLogin(token);
debugPrint('[Login] tokenLogin 结果: $success'); debugPrint('[Login] tokenLogin 结果: $success');
if (success && mounted) { if (success && mounted) {
context.go('/login'); await _navigateAfterLogin();
} }
} }
@ -274,8 +274,25 @@ class _LoginPageState extends ConsumerState<LoginPage> {
.read(authControllerProvider.notifier) .read(authControllerProvider.notifier)
.codeLogin(_phoneController.text, _codeController.text); .codeLogin(_phoneController.text, _codeController.text);
if (success && mounted) { if (success && mounted) {
// /login router redirect await _navigateAfterLogin();
context.go('/login'); }
}
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');
} }
} }

View File

@ -1,361 +1,38 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:http/http.dart' as http;
import 'package:image/image.dart' as img;
/// BLE enum TransferState { idle, transferring, complete, error }
/// APP蓝牙传图接口说明.md
///
/// BLE UUID (16-bit 128-bit):
/// : 0x0B00 00000b00-0000-1000-8000-00805f9b34fb
/// : 0x0B01 00000b01-0000-1000-8000-00805f9b34fb
/// : 0x0B02 00000b02-0000-1000-8000-00805f9b34fb
class BadgeBleTransferService {
/// 360×360 LCD
static const _targetSize = 360;
/// MTU = 512 ble_service_config.h class BadgeTransferState {
/// requestMtu iOS/Web final TransferState transferState;
static const _defaultMtu = 512; final List<ScanResult> scannedDevices;
final BluetoothDevice? selectedDevice;
final double progress;
final String? errorMessage;
/// ATT 3 + GATT Handle 2 const BadgeTransferState({
static const _attOverhead = 3; this.transferState = TransferState.idle,
this.scannedDevices = const [],
this.selectedDevice,
this.progress = 0,
this.errorMessage,
});
/// : (1) + (1) BadgeTransferState copyWith({
static const _frameHeaderSize = 2; TransferState? transferState,
List<ScanResult>? scannedDevices,
/// malloc / fopen BluetoothDevice? selectedDevice,
static const _preambleDelayMs = 50; double? progress,
String? errorMessage,
/// (ms) ESP32 BLE }) {
static const _packetDelayMs = 5; return BadgeTransferState(
transferState: transferState ?? this.transferState,
/// N write-with-response scannedDevices: scannedDevices ?? this.scannedDevices,
static int get _syncInterval => kIsWeb ? 5 : 10; selectedDevice: selectedDevice ?? this.selectedDevice,
progress: progress ?? this.progress,
/// errorMessage: errorMessage ?? this.errorMessage,
static const _maxTransferRetries = 2; );
///
Future<void> connectAndTransfer({
required BluetoothDevice device,
required String imageUrl,
Uint8List? imageBytes,
void Function(double progress, String message)? onProgress,
}) async {
//
onProgress?.call(0.05, '处理图片...');
final jpegBytes = await _prepareImage(imageUrl, imageBytes);
debugPrint('[BLE Transfer] JPEG 大小: ${jpegBytes.length} 字节');
final filename =
'face_${DateTime.now().millisecondsSinceEpoch ~/ 1000}.jpg';
//
for (int attempt = 0; attempt <= _maxTransferRetries; attempt++) {
try {
await _doTransfer(
device: device,
filename: filename,
imageData: jpegBytes,
onProgress: onProgress,
);
return;
} catch (e) {
try {
await device.disconnect();
} catch (_) {}
if (attempt < _maxTransferRetries) {
final wait = 2 * (attempt + 1);
onProgress?.call(
0.0, '传输中断,${wait}秒后重试 (${attempt + 1}/$_maxTransferRetries)...');
debugPrint('[BLE Transfer] 第 ${attempt + 1} 次失败: $e${wait}s 后重试');
await Future.delayed(Duration(seconds: wait));
} else {
rethrow;
}
}
}
}
///
Future<void> _doTransfer({
required BluetoothDevice device,
required String filename,
required Uint8List imageData,
void Function(double progress, String message)? onProgress,
}) async {
StreamSubscription<BluetoothConnectionState>? connSub;
bool disconnected = false;
try {
// 1.
onProgress?.call(0.0, '正在连接设备...');
await device.connect(timeout: const Duration(seconds: 10));
connSub = device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
disconnected = true;
}
});
// 2. MTU
onProgress?.call(0.02, '协商传输参数...');
final mtu = await _negotiateMtu(device);
final chunkSize = mtu - _attOverhead - _frameHeaderSize;
debugPrint('[BLE Transfer] 最终 MTU=$mtu, chunkSize=$chunkSize');
// 3.
onProgress?.call(0.05, '发现服务...');
final services = await device.discoverServices();
for (final s in services) {
debugPrint('[BLE Transfer] 服务: ${s.uuid}');
for (final c in s.characteristics) {
debugPrint(
'[BLE Transfer] 特征: ${c.uuid} (${_propsStr(c.properties)})');
}
}
final imageService = services.firstWhere(
(s) => _matchUuid16(s.uuid.toString(), '0b00'),
orElse: () => throw Exception('未找到图片传输服务 (0x0B00)'),
);
final writeChar = imageService.characteristics.firstWhere(
(c) => _matchUuid16(c.uuid.toString(), '0b01'),
orElse: () => throw Exception('未找到写入特征 (0x0B01)'),
);
// 4.
onProgress?.call(0.10, '开始传输...');
await _sendImage(
writeChar: writeChar,
filename: filename,
imageData: imageData,
chunkSize: chunkSize,
isDisconnected: () => disconnected,
onProgress: (p) {
final overall = 0.10 + p * 0.90;
onProgress?.call(overall, '正在传输...');
},
);
onProgress?.call(1.0, '传输完成');
} finally {
connSub?.cancel();
try {
await device.disconnect();
} catch (_) {}
}
}
/// MTU 512
Future<int> _negotiateMtu(BluetoothDevice device) async {
// 1. Android
try {
final result = await device.requestMtu(_defaultMtu);
debugPrint('[BLE Transfer] requestMtu 返回: $result');
} catch (e) {
debugPrint('[BLE Transfer] requestMtu 不可用iOS/Web 正常): $e');
}
// 2.
int mtu = device.mtuNow;
debugPrint('[BLE Transfer] mtuNow 初始值: $mtu');
if (mtu >= 64) return mtu;
// 3. mtuNow 23
// mtu iOS/Web ms
try {
mtu = await device.mtu
.where((v) => v >= 64) // 23
.first
.timeout(const Duration(seconds: 3));
debugPrint('[BLE Transfer] MTU 流更新: $mtu');
return mtu;
} catch (_) {
debugPrint('[BLE Transfer] 等待 MTU 协商超时,使用设备端约定值 $_defaultMtu');
}
// 4. ble_service_config.h MTU=512
return _defaultMtu;
}
/// 360×360 JPEG
Future<Uint8List> _prepareImage(
String imageUrl, Uint8List? localBytes) async {
// 1.
Uint8List rawBytes;
if (localBytes != null) {
rawBytes = localBytes;
} else if (imageUrl.startsWith('http')) {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('下载图片失败 (${response.statusCode})');
}
rawBytes = response.bodyBytes;
} else if (!kIsWeb) {
final file = File(imageUrl);
if (!await file.exists()) {
throw Exception('本地图片不存在: $imageUrl');
}
rawBytes = await file.readAsBytes();
} else {
throw Exception('无法加载图片: $imageUrl');
}
// 2. isolate JPEG
final jpegBytes = await compute(_processImageToJpeg, rawBytes);
debugPrint(
'[BLE Transfer] 图片处理完成: ${rawBytes.length}${jpegBytes.length} 字节');
return jpegBytes;
}
/// 360×360 JPEG
/// isolate UI
static Uint8List _processImageToJpeg(Uint8List rawBytes) {
final decoded = img.decodeImage(rawBytes);
if (decoded == null) {
throw Exception('图片解码失败,不支持的格式');
}
//
final cropSide =
decoded.width < decoded.height ? decoded.width : decoded.height;
final cropX = (decoded.width - cropSide) ~/ 2;
final cropY = (decoded.height - cropSide) ~/ 2;
img.Image cropped = img.copyCrop(decoded,
x: cropX, y: cropY, width: cropSide, height: cropSide);
//
if (cropped.width != _targetSize || cropped.height != _targetSize) {
cropped = img.copyResize(cropped,
width: _targetSize,
height: _targetSize,
interpolation: img.Interpolation.linear);
}
// JPEGquality 85
return Uint8List.fromList(img.encodeJpg(cropped, quality: 85));
}
/// +
Future<void> _sendImage({
required BluetoothCharacteristic writeChar,
required String filename,
required Uint8List imageData,
required int chunkSize,
required bool Function() isDisconnected,
void Function(double)? onProgress,
}) async {
final len = imageData.length;
final totalPackets = (len / chunkSize).ceil();
debugPrint('[BLE Transfer] 总包数: $totalPackets, chunkSize: $chunkSize');
// 26 write-with-response
final header = Uint8List(26);
header[0] = 0xFD;
final nameBytes = Uint8List.fromList(filename.codeUnits);
for (int i = 0; i < nameBytes.length && i < 22; i++) {
header[i + 1] = nameBytes[i];
}
header[23] = (len >> 16) & 0xFF;
header[24] = (len >> 8) & 0xFF;
header[25] = len & 0xFF;
await _bleWriteWithRetry(writeChar, header, withoutResponse: false);
await Future.delayed(const Duration(milliseconds: _preambleDelayMs));
//
int offset = 0;
int packetNo = 0;
final syncEvery = _syncInterval;
while (offset < len) {
if (isDisconnected()) {
throw Exception(
'BLE 连接断开,传输中止于 $offset/$len 字节 (${(offset * 100 / len).toInt()}%)');
}
final remaining = len - offset;
final chunkLen = remaining < chunkSize ? remaining : chunkSize;
final isEnd = (offset + chunkLen >= len) ? 0x01 : 0x00;
final packet = Uint8List(2 + chunkLen);
packet[0] = packetNo & 0xFF;
packet[1] = isEnd;
packet.setRange(2, 2 + chunkLen, imageData, offset);
// N write-with-response
final useSync = (packetNo % syncEvery == 0) || isEnd == 1;
await _bleWriteWithRetry(writeChar, packet,
withoutResponse: !useSync);
if (!useSync) {
await Future.delayed(const Duration(milliseconds: _packetDelayMs));
}
offset += chunkLen;
packetNo++;
onProgress?.call(offset / len);
}
debugPrint('[BLE Transfer] 发送完成: $offset/$len 字节, $packetNo');
}
/// 16-bit UUID
bool _matchUuid16(String uuid, String short16) {
final u = uuid.toLowerCase();
final s = short16.toLowerCase();
if (u == '0000$s-0000-1000-8000-00805f9b34fb') return true;
if (u == s || u == '0000$s') return true;
if (u.startsWith('0000$s-')) return true;
return false;
}
/// BLE 3 + 退 +
Future<void> _bleWriteWithRetry(
BluetoothCharacteristic characteristic,
Uint8List data, {
bool withoutResponse = true,
}) async {
const maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
try {
await characteristic.write(data, withoutResponse: withoutResponse);
return;
} catch (e) {
if (i < maxRetry - 1) {
await Future.delayed(Duration(milliseconds: 20 * (i + 1)));
} else if (withoutResponse) {
try {
await characteristic.write(data, withoutResponse: false);
return;
} catch (_) {
rethrow;
}
} else {
rethrow;
}
}
}
}
String _propsStr(CharacteristicProperties p) {
final parts = <String>[];
if (p.write) parts.add('write');
if (p.writeWithoutResponse) parts.add('writeNoResp');
if (p.read) parts.add('read');
if (p.notify) parts.add('notify');
if (p.indicate) parts.add('indicate');
return parts.join(' + ');
} }
} }

View File

@ -1,10 +1,6 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../data/services/badge_transfer_service.dart';
part 'badge_transfer_controller.g.dart'; part 'badge_transfer_controller.g.dart';
enum TransferStatus { idle, scanning, connecting, transferring, done, error } enum TransferStatus { idle, scanning, connecting, transferring, done, error }
@ -13,14 +9,12 @@ class BadgeTransferState {
final TransferStatus status; final TransferStatus status;
final BluetoothDevice? device; final BluetoothDevice? device;
final double progress; final double progress;
final String? statusMessage;
final String? errorMessage; final String? errorMessage;
const BadgeTransferState({ const BadgeTransferState({
this.status = TransferStatus.idle, this.status = TransferStatus.idle,
this.device, this.device,
this.progress = 0.0, this.progress = 0.0,
this.statusMessage,
this.errorMessage, this.errorMessage,
}); });
@ -28,14 +22,12 @@ class BadgeTransferState {
TransferStatus? status, TransferStatus? status,
BluetoothDevice? device, BluetoothDevice? device,
double? progress, double? progress,
String? statusMessage,
String? errorMessage, String? errorMessage,
}) { }) {
return BadgeTransferState( return BadgeTransferState(
status: status ?? this.status, status: status ?? this.status,
device: device ?? this.device, device: device ?? this.device,
progress: progress ?? this.progress, progress: progress ?? this.progress,
statusMessage: statusMessage ?? this.statusMessage,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
} }
@ -43,8 +35,6 @@ class BadgeTransferState {
@riverpod @riverpod
class BadgeTransferController extends _$BadgeTransferController { class BadgeTransferController extends _$BadgeTransferController {
final _transferService = BadgeBleTransferService();
@override @override
BadgeTransferState build() { BadgeTransferState build() {
ref.onDispose(() { ref.onDispose(() {
@ -53,50 +43,53 @@ class BadgeTransferController extends _$BadgeTransferController {
return const BadgeTransferState(); return const BadgeTransferState();
} }
/// void startScan() {
if (!ref.mounted) return;
state = state.copyWith(status: TransferStatus.scanning);
FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
FlutterBluePlus.scanResults.listen((results) {
if (!ref.mounted) return;
// Process scan results
});
FlutterBluePlus.adapterState.listen((adapterState) {
if (!ref.mounted) return;
// Handle adapter state changes
});
}
void stopScan() {
FlutterBluePlus.stopScan();
if (!ref.mounted) return;
state = state.copyWith(status: TransferStatus.idle);
}
Future<void> connectAndTransfer( Future<void> connectAndTransfer(
BluetoothDevice device, BluetoothDevice device,
String imageUrl, { String imageUrl,
Uint8List? imageBytes, ) async {
}) async {
if (!ref.mounted) return; if (!ref.mounted) return;
state = state.copyWith( state = state.copyWith(
status: TransferStatus.connecting, status: TransferStatus.connecting,
device: device, device: device,
progress: 0.0,
statusMessage: '正在连接设备...',
); );
try { try {
await _transferService.connectAndTransfer( await device.connect();
device: device, if (!ref.mounted) return;
imageUrl: imageUrl, state = state.copyWith(status: TransferStatus.transferring);
imageBytes: imageBytes, // Transfer logic here
onProgress: (progress, message) { await Future.delayed(const Duration(seconds: 2));
if (!ref.mounted) return;
final newStatus = progress < 0.15
? TransferStatus.connecting
: TransferStatus.transferring;
state = state.copyWith(
status: newStatus,
progress: progress,
statusMessage: message,
);
},
);
if (!ref.mounted) return; if (!ref.mounted) return;
state = state.copyWith( state = state.copyWith(
status: TransferStatus.done, status: TransferStatus.done,
progress: 1.0, progress: 1.0,
statusMessage: '传输完成',
); );
} catch (e) { } catch (e) {
debugPrint('[BadgeTransfer] 传输失败: $e');
if (!ref.mounted) return; if (!ref.mounted) return;
state = state.copyWith( state = state.copyWith(
status: TransferStatus.error, status: TransferStatus.error,
errorMessage: e.toString().replaceFirst('Exception: ', ''), errorMessage: e.toString(),
); );
} }
} }

View File

@ -42,7 +42,7 @@ final class BadgeTransferControllerProvider
} }
String _$badgeTransferControllerHash() => String _$badgeTransferControllerHash() =>
r'5f06a9bcf5c9fcf0caf428497d2b9901b1b7f626'; r'6ed7ce6b7aa6eae541366245e297b1c1ed1413f2';
abstract class _$BadgeTransferController extends $Notifier<BadgeTransferState> { abstract class _$BadgeTransferController extends $Notifier<BadgeTransferState> {
BadgeTransferState build(); BadgeTransferState build();

View File

@ -1,424 +0,0 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/network/api_config.dart';
import '../../../../pages/profile/profile_page.dart';
import '../../../../theme/product_theme.dart';
import '../../../../widgets/animated_gradient_background.dart';
/// (DZBJ-OFF)
class BadgeBasicControlPage extends ConsumerStatefulWidget {
const BadgeBasicControlPage({super.key});
@override
ConsumerState<BadgeBasicControlPage> createState() =>
_BadgeBasicControlPageState();
}
class _BadgeBasicControlPageState extends ConsumerState<BadgeBasicControlPage>
with SingleTickerProviderStateMixin {
int _currentTab = 0;
late AnimationController _floatController;
late Animation<double> _floatAnimation;
String? _lastImageUrl;
bool _loading = true;
@override
void initState() {
super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic));
_floatController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true);
_floatAnimation = Tween<double>(begin: -8, end: 8).animate(
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
);
_loadLastImage();
}
@override
void dispose() {
_floatController.dispose();
super.dispose();
}
Future<void> _loadLastImage() async {
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('access_token');
final resp = await http.get(
Uri.parse('${ApiConfig.fullBaseUrl}/badge/history/'),
headers: {if (token != null) 'Authorization': 'Bearer $token'},
).timeout(const Duration(seconds: 10));
if (resp.statusCode == 200) {
final body = jsonDecode(resp.body) as Map<String, dynamic>;
final data = body['data'] as Map<String, dynamic>? ?? {};
final images = (data['images'] as List<dynamic>? ?? [])
.cast<Map<String, dynamic>>()
.where((img) =>
img['generation_status'] == 'completed' &&
(img['image_url'] as String?)?.isNotEmpty == true)
.toList();
if (images.isNotEmpty && mounted) {
setState(() => _lastImageUrl = images.first['image_url'] as String);
}
}
} catch (_) {}
if (mounted) setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
final productTheme = ref.watch(currentProductThemeProvider);
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
const AnimatedGradientBackground(),
IndexedStack(
index: _currentTab,
children: [
_buildHomePage(productTheme),
const ProfilePage(),
],
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 12,
child: _buildBottomNavBar(productTheme),
),
],
),
);
}
Widget _buildHomePage(ProductThemeData productTheme) {
return SafeArea(
bottom: false,
child: Stack(
children: [
Center(
child: AnimatedBuilder(
animation: _floatAnimation,
builder: (context, child) => Transform.translate(
offset: Offset(0, _floatAnimation.value),
child: child,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_BadgePreviewCircle(
imageUrl: _lastImageUrl,
loading: _loading,
accentColor: productTheme.accentColor,
size: 240,
),
const SizedBox(height: 16),
if (!_loading && _lastImageUrl == null)
Text(
'点击右上角「传图」上传你的第一张图',
style: TextStyle(
fontSize: 13,
color: productTheme.accentColor.withOpacity(0.5),
),
),
],
),
),
),
Positioned(
top: 8,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_GlassIconButton(
onTap: () => context.push('/product-selection'),
child: SvgPicture.asset(
'assets/www/icons/icon-switch.svg',
width: 20,
height: 20,
colorFilter: const ColorFilter.mode(
Color(0xFF4B5563),
BlendMode.srcIn,
),
),
),
_GlassPillButton(
onTap: () => context.push('/badge'),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'传图',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: productTheme.accentColor,
),
),
const SizedBox(width: 4),
Icon(Icons.send_rounded,
size: 16, color: productTheme.accentColor),
],
),
),
],
),
),
],
),
);
}
Widget _buildBottomNavBar(ProductThemeData productTheme) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
width: 180,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(32),
border: Border.all(color: Colors.white.withOpacity(0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF4B5563).withOpacity(0.08),
offset: const Offset(0, 10),
blurRadius: 30,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(0, 'assets/www/icons/icon-home-pixel.svg',
Icons.home, productTheme),
_buildNavItem(1, 'assets/www/icons/icon-user-pixel.svg',
Icons.person, productTheme),
],
),
),
),
),
);
}
Widget _buildNavItem(
int index, String iconPath, IconData fallback, ProductThemeData theme) {
final isActive = _currentTab == index;
return GestureDetector(
onTap: () => setState(() => _currentTab = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: isActive ? theme.buttonGradient : null,
borderRadius: BorderRadius.circular(28),
boxShadow: isActive
? [
BoxShadow(
color: theme.accentColor.withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 15,
),
]
: null,
),
alignment: Alignment.center,
child: SvgPicture.asset(
iconPath,
width: isActive ? 28 : 26,
height: isActive ? 28 : 26,
colorFilter: ColorFilter.mode(
isActive ? Colors.white : const Color(0xFF6B7280).withOpacity(0.6),
BlendMode.srcIn,
),
placeholderBuilder: (_) => Icon(
fallback,
color: isActive ? Colors.white : const Color(0xFF6B7280),
size: 24,
),
),
),
);
}
}
class _BadgePreviewCircle extends StatelessWidget {
final String? imageUrl;
final bool loading;
final Color accentColor;
final double size;
const _BadgePreviewCircle({
required this.imageUrl,
required this.loading,
required this.accentColor,
required this.size,
});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF2D3748),
border: Border.all(color: const Color(0xFF4A5568), width: 4),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.2),
blurRadius: 40,
spreadRadius: 8,
),
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
padding: const EdgeInsets.all(8),
child: ClipOval(
child: loading
? Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: accentColor.withOpacity(0.5),
),
)
: imageUrl != null
? Image.network(
imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(),
),
);
}
Widget _buildPlaceholder() {
return Container(
color: const Color(0xFF1A202C),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add_photo_alternate_outlined,
size: 48, color: accentColor.withOpacity(0.4)),
const SizedBox(height: 8),
Text(
'暂无图片',
style: TextStyle(
fontSize: 13,
color: accentColor.withOpacity(0.3),
),
),
],
),
),
);
}
}
class _GlassIconButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassIconButton({required this.onTap, required this.child});
@override
State<_GlassIconButton> createState() => _GlassIconButtonState();
}
class _GlassIconButtonState extends State<_GlassIconButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.92 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
alignment: Alignment.center,
child: widget.child,
),
),
);
}
}
class _GlassPillButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassPillButton({required this.onTap, required this.child});
@override
State<_GlassPillButton> createState() => _GlassPillButtonState();
}
class _GlassPillButtonState extends State<_GlassPillButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.94 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
child: widget.child,
),
),
);
}
}

View File

@ -1,320 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import '../../../../pages/profile/profile_page.dart';
import '../../../../theme/product_theme.dart';
import '../../../../widgets/animated_gradient_background.dart';
/// AI (DZBJ-ON)
class BadgeControlPage extends ConsumerStatefulWidget {
const BadgeControlPage({super.key});
@override
ConsumerState<BadgeControlPage> createState() => _BadgeControlPageState();
}
class _BadgeControlPageState extends ConsumerState<BadgeControlPage>
with SingleTickerProviderStateMixin {
int _currentTab = 0; // 0: , 1:
late AnimationController _floatController;
late Animation<double> _floatAnimation;
@override
void initState() {
super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeAi));
_floatController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true);
_floatAnimation = Tween<double>(begin: -8, end: 8).animate(
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_floatController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final productTheme = ref.watch(currentProductThemeProvider);
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
const AnimatedGradientBackground(),
//
IndexedStack(
index: _currentTab,
children: [
_buildHomePage(productTheme),
const ProfilePage(),
],
),
//
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 12,
child: _buildBottomNavBar(productTheme),
),
],
),
);
}
Widget _buildHomePage(ProductThemeData productTheme) {
return SafeArea(
bottom: false,
child: Stack(
children: [
//
Center(
child: AnimatedBuilder(
animation: _floatAnimation,
builder: (context, child) => Transform.translate(
offset: Offset(0, _floatAnimation.value),
child: child,
),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: productTheme.accentColor.withOpacity(0.2),
blurRadius: 60,
spreadRadius: 15,
),
BoxShadow(
color: productTheme.accentColorLight.withOpacity(0.1),
blurRadius: 100,
spreadRadius: 30,
),
],
),
child: Image.asset(
'assets/www/Capybara.png',
width: 260,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Icon(
Icons.smart_toy,
size: 150,
color: productTheme.accentColor),
),
),
),
),
//
Positioned(
top: 8,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_GlassIconButton(
onTap: () => context.push('/product-selection'),
child: SvgPicture.asset(
'assets/www/icons/icon-switch.svg',
width: 20,
height: 20,
colorFilter: const ColorFilter.mode(
Color(0xFF4B5563),
BlendMode.srcIn,
),
),
),
_GlassPillButton(
onTap: () => context.push('/badge'),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'传图',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: productTheme.accentColor,
),
),
const SizedBox(width: 4),
Icon(Icons.send_rounded,
size: 16, color: productTheme.accentColor),
],
),
),
],
),
),
],
),
);
}
Widget _buildBottomNavBar(ProductThemeData productTheme) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
width: 180,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(32),
border: Border.all(color: Colors.white.withOpacity(0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF4B5563).withOpacity(0.08),
offset: const Offset(0, 10),
blurRadius: 30,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(0, 'assets/www/icons/icon-home-pixel.svg',
Icons.home, productTheme),
_buildNavItem(1, 'assets/www/icons/icon-user-pixel.svg',
Icons.person, productTheme),
],
),
),
),
),
);
}
Widget _buildNavItem(
int index, String iconPath, IconData fallback, ProductThemeData theme) {
final isActive = _currentTab == index;
return GestureDetector(
onTap: () => setState(() => _currentTab = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: isActive ? theme.buttonGradient : null,
borderRadius: BorderRadius.circular(28),
boxShadow: isActive
? [
BoxShadow(
color: theme.accentColor.withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 15,
),
]
: null,
),
alignment: Alignment.center,
child: SvgPicture.asset(
iconPath,
width: isActive ? 28 : 26,
height: isActive ? 28 : 26,
colorFilter: ColorFilter.mode(
isActive ? Colors.white : const Color(0xFF6B7280).withOpacity(0.6),
BlendMode.srcIn,
),
placeholderBuilder: (_) => Icon(
fallback,
color: isActive ? Colors.white : const Color(0xFF6B7280),
size: 24,
),
),
),
);
}
}
class _GlassIconButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassIconButton({required this.onTap, required this.child});
@override
State<_GlassIconButton> createState() => _GlassIconButtonState();
}
class _GlassIconButtonState extends State<_GlassIconButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.92 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
alignment: Alignment.center,
child: widget.child,
),
),
);
}
}
class _GlassPillButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassPillButton({required this.onTap, required this.child});
@override
State<_GlassPillButton> createState() => _GlassPillButtonState();
}
class _GlassPillButtonState extends State<_GlassPillButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.94 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
child: widget.child,
),
),
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../theme/product_theme.dart';
import '../../../../widgets/animated_gradient_background.dart'; import '../../../../widgets/animated_gradient_background.dart';
import '../../data/services/badge_ai_generation_service.dart'; import '../../data/services/badge_ai_generation_service.dart';
import '../controllers/badge_ai_controller.dart'; import '../controllers/badge_ai_controller.dart';
@ -24,13 +23,13 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
// //
String? _uploadedImagePath; String? _uploadedImagePath;
Uint8List? _uploadedImageBytes;
// AI // AI
bool _isGenerating = false; bool _isGenerating = false;
double _genProgress = 0; double _genProgress = 0;
String _genStatus = ''; String _genStatus = '';
String? _generatedImageUrl; String? _generatedImageUrl;
bool _hasAiResult = false;
@override @override
void initState() { void initState() {
@ -44,6 +43,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
final pending = BadgeAiGenerationService.instance.consumePendingResult(); final pending = BadgeAiGenerationService.instance.consumePendingResult();
if (pending != null) { if (pending != null) {
_generatedImageUrl = pending.imageUrl; _generatedImageUrl = pending.imageUrl;
_hasAiResult = true;
} }
// AI // AI
@ -61,6 +61,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
setState(() { setState(() {
_isGenerating = false; _isGenerating = false;
_generatedImageUrl = result.imageUrl; _generatedImageUrl = result.imageUrl;
_hasAiResult = true;
}); });
_showResultDialog(result.imageUrl); _showResultDialog(result.imageUrl);
} }
@ -86,7 +87,6 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
} }
void _showResultDialog(String imageUrl) { void _showResultDialog(String imageUrl) {
final theme = ref.read(currentProductThemeProvider);
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -163,7 +163,14 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
height: 48, height: 48,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: theme.buttonGradient, gradient: const LinearGradient(
colors: [
Color(0xFF22D3EE),
Color(0xFF3B82F6),
Color(0xFF6366F1),
Color(0xFF8B5CF6),
],
),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
child: ElevatedButton( child: ElevatedButton(
@ -206,6 +213,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
setState(() { setState(() {
_isGenerating = true; _isGenerating = true;
_generatedImageUrl = null; _generatedImageUrl = null;
_hasAiResult = false;
_genProgress = 0; _genProgress = 0;
_genStatus = '正在连接 AI...'; _genStatus = '正在连接 AI...';
}); });
@ -228,30 +236,24 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
void _handleUploadSelected(String path, Uint8List? bytes) { void _handleUploadSelected(String path, Uint8List? bytes) {
setState(() { setState(() {
_uploadedImagePath = path; _uploadedImagePath = path;
_uploadedImageBytes = bytes;
}); });
} }
void _handleRetry() { void _handleRetry() {
setState(() { setState(() {
_generatedImageUrl = null; _generatedImageUrl = null;
_hasAiResult = false;
}); });
} }
void _handleUseImage() { void _handleUseImage() {
final imageSource = _generatedImageUrl ?? _uploadedImagePath; final imageSource = _generatedImageUrl ?? _uploadedImagePath;
if (imageSource == null) return; if (imageSource == null) return;
context.push('/badge/transfer', extra: { context.push('/badge/transfer', extra: {'imageUrl': imageSource});
'imageUrl': imageSource,
if (_uploadedImageBytes != null && _generatedImageUrl == null)
'imageBytes': _uploadedImageBytes,
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final productTheme = ref.watch(currentProductThemeProvider);
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: Stack( body: Stack(
@ -261,13 +263,13 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
child: Column( child: Column(
children: [ children: [
_buildHeader(), _buildHeader(),
_buildTabBar(productTheme), _buildTabBar(),
Expanded(child: _buildTabContent(productTheme)), Expanded(child: _buildTabContent()),
], ],
), ),
), ),
if (_isGenerating) _buildGeneratingOverlay(productTheme), if (_isGenerating) _buildGeneratingOverlay(),
_buildFixedBottomBar(productTheme), _buildFixedBottomBar(),
], ],
), ),
); );
@ -310,7 +312,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
); );
} }
Widget _buildTabBar(ProductThemeData productTheme) { Widget _buildTabBar() {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
@ -326,7 +328,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: productTheme.accentColor.withOpacity(0.15), color: const Color(0xFF6366F1).withOpacity(0.15),
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@ -334,7 +336,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
), ),
indicatorSize: TabBarIndicatorSize.tab, indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
labelColor: productTheme.accentColor, labelColor: const Color(0xFF6366F1),
unselectedLabelColor: const Color(0xFF6B7280), unselectedLabelColor: const Color(0xFF6B7280),
labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), unselectedLabelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
@ -346,7 +348,7 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
); );
} }
Widget _buildTabContent(ProductThemeData productTheme) { Widget _buildTabContent() {
return TabBarView( return TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
@ -354,17 +356,13 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
key: _aiTabKey, key: _aiTabKey,
onGenerate: _handleAiGenerate, onGenerate: _handleAiGenerate,
isGenerating: _isGenerating, isGenerating: _isGenerating,
accentColor: productTheme.accentColor,
),
BadgeUploadTab(
onImageSelected: _handleUploadSelected,
accentColor: productTheme.accentColor,
), ),
BadgeUploadTab(onImageSelected: _handleUploadSelected),
], ],
); );
} }
Widget _buildGeneratingOverlay(ProductThemeData productTheme) { Widget _buildGeneratingOverlay() {
return Positioned.fill( return Positioned.fill(
child: Container( child: Container(
color: Colors.black.withOpacity(0.4), color: Colors.black.withOpacity(0.4),
@ -379,21 +377,21 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( const SizedBox(
width: 48, width: 48,
height: 48, height: 48,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: productTheme.accentColor, color: Color(0xFF6366F1),
strokeWidth: 3, strokeWidth: 3,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_genStatus, _genStatus,
style: TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: productTheme.accentColor, color: Color(0xFF6366F1),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -401,8 +399,8 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: _genProgress / 100, value: _genProgress / 100,
backgroundColor: productTheme.accentColor.withOpacity(0.15), backgroundColor: const Color(0xFF6366F1).withOpacity(0.15),
valueColor: AlwaysStoppedAnimation(productTheme.accentColor), valueColor: const AlwaysStoppedAnimation(Color(0xFF6366F1)),
minHeight: 4, minHeight: 4,
), ),
), ),
@ -415,40 +413,38 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
} }
/// ///
Widget _buildFixedBottomBar(ProductThemeData productTheme) { Widget _buildFixedBottomBar() {
final isAiTab = _tabController.index == 0; final isAiTab = _tabController.index == 0;
final isUploadTab = _tabController.index == 1; final isUploadTab = _tabController.index == 1;
Widget? buttonContent; Widget? buttonContent;
if (isAiTab) { if (isAiTab) {
buttonContent = _buildGradientButton( // "开始生成"
'开始生成', if (!_isGenerating && !_hasAiResult) {
_isGenerating buttonContent = _buildGradientButton('开始生成', () {
? null final aiState = _aiTabKey.currentState;
: () { if (aiState == null) return;
final aiState = _aiTabKey.currentState; final prompt = aiState.currentPrompt;
if (aiState == null) return; final isI2I = aiState.referenceImageBytes != null;
final prompt = aiState.currentPrompt; //
final isI2I = aiState.referenceImageBytes != null; if (!isI2I && prompt.isEmpty) {
// ScaffoldMessenger.of(context).showSnackBar(
if (!isI2I && prompt.isEmpty) { const SnackBar(
ScaffoldMessenger.of(context).showSnackBar( content: Text('请输入图片描述'),
const SnackBar( backgroundColor: Colors.orange,
content: Text('请输入图片描述'), ),
backgroundColor: Colors.orange, );
), return;
); }
return; _handleAiGenerate(
} prompt: prompt,
_handleAiGenerate( style: aiState.selectedStyle,
prompt: prompt, imageBytes: aiState.referenceImageBytes,
style: aiState.selectedStyle, strength: aiState.strength,
imageBytes: aiState.referenceImageBytes, );
strength: aiState.strength, });
); }
},
);
} else if (isUploadTab) { } else if (isUploadTab) {
if (_uploadedImagePath != null) { if (_uploadedImagePath != null) {
buttonContent = _buildGradientButton('使用此图', _handleUseImage); buttonContent = _buildGradientButton('使用此图', _handleUseImage);
@ -478,13 +474,14 @@ class _BadgeHomePageState extends ConsumerState<BadgeHomePage>
); );
} }
Widget _buildGradientButton(String label, VoidCallback? onPressed) { Widget _buildGradientButton(String label, VoidCallback onPressed) {
final theme = ref.read(currentProductThemeProvider);
return SizedBox( return SizedBox(
height: 52, height: 52,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: theme.buttonGradient, gradient: const LinearGradient(
colors: [Color(0xFF22D3EE), Color(0xFF3B82F6), Color(0xFF6366F1), Color(0xFF8B5CF6)],
),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: ElevatedButton( child: ElevatedButton(

View File

@ -12,13 +12,11 @@ class BadgeAiTab extends StatefulWidget {
double strength, double strength,
}) onGenerate; }) onGenerate;
final bool isGenerating; final bool isGenerating;
final Color accentColor;
const BadgeAiTab({ const BadgeAiTab({
super.key, super.key,
required this.onGenerate, required this.onGenerate,
this.isGenerating = false, this.isGenerating = false,
this.accentColor = const Color(0xFF6366F1),
}); });
@override @override
@ -66,26 +64,25 @@ class BadgeAiTabState extends State<BadgeAiTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accent = widget.accentColor;
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// //
_buildModeToggle(accent), _buildModeToggle(),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
if (_isI2I) ...[ if (_isI2I) ...[
_buildReferenceImageSection(accent), _buildReferenceImageSection(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildStrengthSlider(accent), _buildStrengthSlider(),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// //
_buildPromptInput(accent), _buildPromptInput(),
const SizedBox(height: 20), const SizedBox(height: 20),
// //
@ -104,35 +101,35 @@ class BadgeAiTabState extends State<BadgeAiTab> {
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStyleGrid(accent), _buildStyleGrid(),
], ],
), ),
); );
} }
Widget _buildModeToggle(Color accent) { Widget _buildModeToggle() {
return Row( return Row(
children: [ children: [
_buildModeBtn('文生图', !_isI2I, () => setState(() => _isI2I = false), accent), _buildModeBtn('文生图', !_isI2I, () => setState(() => _isI2I = false)),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildModeBtn('图生图', _isI2I, () => setState(() => _isI2I = true), accent), _buildModeBtn('图生图', _isI2I, () => setState(() => _isI2I = true)),
], ],
); );
} }
Widget _buildModeBtn(String label, bool active, VoidCallback onTap, Color accent) { Widget _buildModeBtn(String label, bool active, VoidCallback onTap) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: active ? accent : Colors.transparent, color: active ? const Color(0xFF6366F1) : Colors.transparent,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: active color: active
? accent ? const Color(0xFF6366F1)
: accent.withOpacity(0.2), : const Color(0xFF6366F1).withOpacity(0.2),
width: 1.5, width: 1.5,
), ),
), ),
@ -148,7 +145,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
); );
} }
Widget _buildReferenceImageSection(Color accent) { Widget _buildReferenceImageSection() {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -207,7 +204,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
height: 180, height: 180,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: accent.withOpacity(0.25), color: const Color(0xFF6366F1).withOpacity(0.25),
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@ -216,7 +213,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.add, size: 36, color: accent.withOpacity(0.6)), Icon(Icons.add, size: 36, color: const Color(0xFFA78BFA)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text('点击上传参考图', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280))), const Text('点击上传参考图', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280))),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -230,7 +227,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
); );
} }
Widget _buildStrengthSlider(Color accent) { Widget _buildStrengthSlider() {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -249,18 +246,18 @@ class BadgeAiTabState extends State<BadgeAiTab> {
), ),
Text( Text(
(_strength).toStringAsFixed(1), (_strength).toStringAsFixed(1),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: accent), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF6366F1)),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SliderTheme( SliderTheme(
data: SliderTheme.of(context).copyWith( data: SliderTheme.of(context).copyWith(
activeTrackColor: accent, activeTrackColor: const Color(0xFF6366F1),
inactiveTrackColor: accent.withOpacity(0.15), inactiveTrackColor: const Color(0xFF6366F1).withOpacity(0.15),
thumbColor: Colors.white, thumbColor: Colors.white,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 11), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 11),
overlayColor: accent.withOpacity(0.1), overlayColor: const Color(0xFF6366F1).withOpacity(0.1),
trackHeight: 6, trackHeight: 6,
), ),
child: Slider( child: Slider(
@ -283,7 +280,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
); );
} }
Widget _buildPromptInput(Color accent) { Widget _buildPromptInput() {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -318,7 +315,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: accent), borderSide: const BorderSide(color: Color(0xFFA78BFA)),
), ),
contentPadding: const EdgeInsets.all(14), contentPadding: const EdgeInsets.all(14),
), ),
@ -328,7 +325,7 @@ class BadgeAiTabState extends State<BadgeAiTab> {
); );
} }
Widget _buildStyleGrid(Color accent) { Widget _buildStyleGrid() {
return GridView.builder( return GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -344,7 +341,6 @@ class BadgeAiTabState extends State<BadgeAiTab> {
return BadgeStyleChip( return BadgeStyleChip(
style: style, style: style,
selected: _selectedStyle == style.id, selected: _selectedStyle == style.id,
accentColor: accent,
onTap: () { onTap: () {
setState(() { setState(() {
_selectedStyle = _selectedStyle == style.id ? null : style.id; _selectedStyle = _selectedStyle == style.id ? null : style.id;

View File

@ -1,23 +1,25 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class BadgeBleDeviceCard extends StatelessWidget { class BadgeBleDeviceCard extends StatelessWidget {
final String displayName; final ScanResult scanResult;
final int rssi;
final bool selected; final bool selected;
final VoidCallback onTap; final VoidCallback onTap;
final Color accentColor;
const BadgeBleDeviceCard({ const BadgeBleDeviceCard({
super.key, super.key,
required this.displayName, required this.scanResult,
required this.rssi,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
this.accentColor = const Color(0xFF8B5CF6),
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final name = scanResult.device.platformName.isNotEmpty
? scanResult.device.platformName
: '未知设备';
final rssi = scanResult.rssi;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
@ -25,12 +27,12 @@ class BadgeBleDeviceCard extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected
? accentColor.withOpacity(0.08) ? const Color(0xFF8B5CF6).withOpacity(0.08)
: Colors.white.withOpacity(0.7), : Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: selected color: selected
? accentColor ? const Color(0xFF8B5CF6)
: Colors.black.withOpacity(0.06), : Colors.black.withOpacity(0.06),
width: selected ? 2 : 1, width: selected ? 2 : 1,
), ),
@ -40,7 +42,7 @@ class BadgeBleDeviceCard extends StatelessWidget {
Icon( Icon(
Icons.bluetooth, Icons.bluetooth,
color: selected color: selected
? accentColor ? const Color(0xFF8B5CF6)
: const Color(0xFF9CA3AF), : const Color(0xFF9CA3AF),
size: 22, size: 22,
), ),
@ -50,12 +52,12 @@ class BadgeBleDeviceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
displayName, name,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: selected color: selected
? accentColor ? const Color(0xFF8B5CF6)
: const Color(0xFF1F2937), : const Color(0xFF1F2937),
), ),
), ),
@ -70,7 +72,7 @@ class BadgeBleDeviceCard extends StatelessWidget {
), ),
), ),
if (selected) if (selected)
Icon(Icons.check_circle, color: accentColor, size: 22), const Icon(Icons.check_circle, color: Color(0xFF8B5CF6), size: 22),
], ],
), ),
), ),

View File

@ -5,14 +5,12 @@ class BadgeStyleChip extends StatelessWidget {
final BadgeStyle style; final BadgeStyle style;
final bool selected; final bool selected;
final VoidCallback onTap; final VoidCallback onTap;
final Color accentColor;
const BadgeStyleChip({ const BadgeStyleChip({
super.key, super.key,
required this.style, required this.style,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
this.accentColor = const Color(0xFF6366F1),
}); });
@override @override
@ -23,12 +21,12 @@ class BadgeStyleChip extends StatelessWidget {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected
? accentColor.withOpacity(0.1) ? const Color(0xFF6366F1).withOpacity(0.1)
: Colors.white.withOpacity(0.65), : Colors.white.withOpacity(0.65),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all( border: Border.all(
color: selected color: selected
? accentColor ? const Color(0xFF6366F1)
: Colors.white.withOpacity(0.4), : Colors.white.withOpacity(0.4),
width: selected ? 2 : 1, width: selected ? 2 : 1,
), ),
@ -44,7 +42,7 @@ class BadgeStyleChip extends StatelessWidget {
fontSize: 11, fontSize: 11,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
color: selected color: selected
? accentColor ? const Color(0xFF6366F1)
: const Color(0xFF6B7280), : const Color(0xFF6B7280),
), ),
), ),

View File

@ -8,12 +8,10 @@ import '../../../../core/network/api_config.dart';
class BadgeUploadTab extends StatefulWidget { class BadgeUploadTab extends StatefulWidget {
final void Function(String imagePath, Uint8List? bytes) onImageSelected; final void Function(String imagePath, Uint8List? bytes) onImageSelected;
final Color accentColor;
const BadgeUploadTab({ const BadgeUploadTab({
super.key, super.key,
required this.onImageSelected, required this.onImageSelected,
this.accentColor = const Color(0xFF6366F1),
}); });
@override @override
@ -68,7 +66,6 @@ class _BadgeUploadTabState extends State<BadgeUploadTab> {
Navigator.of(context).pop(); Navigator.of(context).pop();
_selectAiImage(url); _selectAiImage(url);
}, },
accentColor: widget.accentColor,
), ),
); );
} }
@ -183,7 +180,7 @@ class _BadgeUploadTabState extends State<BadgeUploadTab> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(icon, color: widget.accentColor, size: 48), Icon(icon, color: const Color(0xFF6366F1), size: 48),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
label, label,
@ -203,12 +200,8 @@ class _BadgeUploadTabState extends State<BadgeUploadTab> {
class _AiHistoryBottomSheet extends StatefulWidget { class _AiHistoryBottomSheet extends StatefulWidget {
final void Function(String imageUrl) onSelect; final void Function(String imageUrl) onSelect;
final Color accentColor;
const _AiHistoryBottomSheet({ const _AiHistoryBottomSheet({required this.onSelect});
required this.onSelect,
this.accentColor = const Color(0xFF6366F1),
});
@override @override
State<_AiHistoryBottomSheet> createState() => _AiHistoryBottomSheetState(); State<_AiHistoryBottomSheet> createState() => _AiHistoryBottomSheetState();
@ -293,8 +286,8 @@ class _AiHistoryBottomSheetState extends State<_AiHistoryBottomSheet> {
Widget _buildContent() { Widget _buildContent() {
if (_loading) { if (_loading) {
return Center( return const Center(
child: CircularProgressIndicator(color: widget.accentColor), child: CircularProgressIndicator(color: Color(0xFF6366F1)),
); );
} }
if (_error != null) { if (_error != null) {

View File

@ -1,178 +1,79 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// class TransferProgressRing extends StatelessWidget {
class TransferProgressRing extends StatefulWidget {
final double progress; final double progress;
final bool isComplete; final bool isComplete;
final Color accentColor;
const TransferProgressRing({ const TransferProgressRing({
super.key, super.key,
required this.progress, required this.progress,
this.isComplete = false, this.isComplete = false,
this.accentColor = const Color(0xFF8B5CF6),
}); });
@override
State<TransferProgressRing> createState() => _TransferProgressRingState();
}
class _TransferProgressRingState extends State<TransferProgressRing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _progressAnim;
double _oldProgress = 0.0;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_progressAnim = Tween<double>(begin: 0, end: widget.progress)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.forward();
}
@override
void didUpdateWidget(TransferProgressRing oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.progress != widget.progress) {
_oldProgress = _progressAnim.value;
_progressAnim = Tween<double>(begin: _oldProgress, end: widget.progress)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller
..reset()
..forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( return SizedBox(
animation: _progressAnim, width: 120,
builder: (context, child) { height: 120,
final p = _progressAnim.value.clamp(0.0, 1.0); child: CustomPaint(
return SizedBox( painter: _ProgressRingPainter(
width: 120, progress: progress,
height: 120, isComplete: isComplete,
child: CustomPaint( ),
painter: _GradientRingPainter( child: Center(
progress: p, child: isComplete
isComplete: widget.isComplete, ? const Icon(Icons.check, color: Color(0xFF10B981), size: 48)
accentColor: widget.accentColor, : Text(
), '${(progress * 100).toInt()}%',
child: Center( style: const TextStyle(
child: widget.isComplete fontSize: 24,
? TweenAnimationBuilder<double>( fontWeight: FontWeight.w700,
tween: Tween(begin: 0.0, end: 1.0), color: Color(0xFF8B5CF6),
duration: const Duration(milliseconds: 400), ),
curve: Curves.elasticOut, ),
builder: (context, value, child) { ),
return Transform.scale( ),
scale: value,
child: child,
);
},
child: Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Color(0xFF10B981),
shape: BoxShape.circle,
),
child: const Icon(Icons.check_rounded,
color: Colors.white, size: 32),
),
)
: Text(
'${(p * 100).toInt()}%',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: widget.accentColor,
),
),
),
),
);
},
); );
} }
} }
class _GradientRingPainter extends CustomPainter { class _ProgressRingPainter extends CustomPainter {
final double progress; final double progress;
final bool isComplete; final bool isComplete;
final Color accentColor;
_GradientRingPainter({ _ProgressRingPainter({required this.progress, required this.isComplete});
required this.progress,
required this.isComplete,
required this.accentColor,
});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 6; final radius = size.width / 2 - 6;
final rect = Rect.fromCircle(center: center, radius: radius);
// // Background ring
final bgPaint = Paint() final bgPaint = Paint()
..color = accentColor.withOpacity(0.1) ..color = const Color(0xFFE5E7EB)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 8 ..strokeWidth = 8
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint); canvas.drawCircle(center, radius, bgPaint);
if (progress <= 0) return; // Progress arc
final color = isComplete ? const Color(0xFF10B981) : const Color(0xFF8B5CF6);
// final fgPaint = Paint()
final sweepAngle = 2 * pi * progress; ..color = color
if (isComplete) {
final completePaint = Paint()
..color = const Color(0xFF10B981)
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, -pi / 2, sweepAngle, false, completePaint);
return;
}
//
final gradient = SweepGradient(
startAngle: -pi / 2,
endAngle: -pi / 2 + sweepAngle,
colors: [
accentColor.withOpacity(0.4),
accentColor,
],
stops: const [0.0, 1.0],
transform: const GradientRotation(-pi / 2),
);
final gradientPaint = Paint()
..shader = gradient.createShader(rect)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 8 ..strokeWidth = 8
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
canvas.drawArc(
canvas.drawArc(rect, -pi / 2, sweepAngle, false, gradientPaint); Rect.fromCircle(center: center, radius: radius),
-pi / 2,
2 * pi * progress,
false,
fgPaint,
);
} }
@override @override
bool shouldRepaint(covariant _GradientRingPainter oldDelegate) => bool shouldRepaint(covariant _ProgressRingPainter oldDelegate) =>
oldDelegate.progress != progress || oldDelegate.progress != progress || oldDelegate.isComplete != isComplete;
oldDelegate.isComplete != isComplete ||
oldDelegate.accentColor != accentColor;
} }

View File

@ -3,23 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'device.freezed.dart'; part 'device.freezed.dart';
part 'device.g.dart'; part 'device.g.dart';
/// API ID
class _SafeDeviceTypeConverter
implements JsonConverter<DeviceType?, Object?> {
const _SafeDeviceTypeConverter();
@override
DeviceType? fromJson(Object? json) {
if (json is Map<String, dynamic>) {
return DeviceType.fromJson(json);
}
return null; // ID null
}
@override
Object? toJson(DeviceType? object) => object?.toJson();
}
@freezed @freezed
abstract class DeviceType with _$DeviceType { abstract class DeviceType with _$DeviceType {
const factory DeviceType({ const factory DeviceType({
@ -41,8 +24,8 @@ abstract class DeviceInfo with _$DeviceInfo {
const factory DeviceInfo({ const factory DeviceInfo({
required int id, required int id,
required String sn, required String sn,
@_SafeDeviceTypeConverter() DeviceType? deviceType, DeviceType? deviceType,
@_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, DeviceType? deviceTypeInfo,
String? macAddress, String? macAddress,
@Default('') String name, @Default('') String name,
@Default('in_stock') String status, @Default('in_stock') String status,

View File

@ -296,7 +296,7 @@ as String?,
/// @nodoc /// @nodoc
mixin _$DeviceInfo { mixin _$DeviceInfo {
int get id; String get sn;@_SafeDeviceTypeConverter() DeviceType? get deviceType;@_SafeDeviceTypeConverter() DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; bool get isOnline; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt; int get id; String get sn; DeviceType? get deviceType; DeviceType? get deviceTypeInfo; String? get macAddress; String get name; String get status; bool get isOnline; String get firmwareVersion; String? get lastOnlineAt; String? get createdAt;
/// Create a copy of DeviceInfo /// Create a copy of DeviceInfo
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -329,7 +329,7 @@ abstract mixin class $DeviceInfoCopyWith<$Res> {
factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl; factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
int id, String sn,@_SafeDeviceTypeConverter() DeviceType? deviceType,@_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
}); });
@ -468,7 +468,7 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, @_SafeDeviceTypeConverter() DeviceType? deviceType, @_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo() when $default != null: case _DeviceInfo() when $default != null:
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
@ -489,7 +489,7 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, @_SafeDeviceTypeConverter() DeviceType? deviceType, @_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo(): case _DeviceInfo():
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
@ -509,7 +509,7 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, @_SafeDeviceTypeConverter() DeviceType? deviceType, @_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _DeviceInfo() when $default != null: case _DeviceInfo() when $default != null:
return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _: return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.macAddress,_that.name,_that.status,_that.isOnline,_that.firmwareVersion,_that.lastOnlineAt,_that.createdAt);case _:
@ -524,13 +524,13 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
@JsonSerializable() @JsonSerializable()
class _DeviceInfo implements DeviceInfo { class _DeviceInfo implements DeviceInfo {
const _DeviceInfo({required this.id, required this.sn, @_SafeDeviceTypeConverter() this.deviceType, @_SafeDeviceTypeConverter() this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.isOnline = false, this.firmwareVersion = '', this.lastOnlineAt, this.createdAt}); const _DeviceInfo({required this.id, required this.sn, this.deviceType, this.deviceTypeInfo, this.macAddress, this.name = '', this.status = 'in_stock', this.isOnline = false, this.firmwareVersion = '', this.lastOnlineAt, this.createdAt});
factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json); factory _DeviceInfo.fromJson(Map<String, dynamic> json) => _$DeviceInfoFromJson(json);
@override final int id; @override final int id;
@override final String sn; @override final String sn;
@override@_SafeDeviceTypeConverter() final DeviceType? deviceType; @override final DeviceType? deviceType;
@override@_SafeDeviceTypeConverter() final DeviceType? deviceTypeInfo; @override final DeviceType? deviceTypeInfo;
@override final String? macAddress; @override final String? macAddress;
@override@JsonKey() final String name; @override@JsonKey() final String name;
@override@JsonKey() final String status; @override@JsonKey() final String status;
@ -572,7 +572,7 @@ abstract mixin class _$DeviceInfoCopyWith<$Res> implements $DeviceInfoCopyWith<$
factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl; factory _$DeviceInfoCopyWith(_DeviceInfo value, $Res Function(_DeviceInfo) _then) = __$DeviceInfoCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
int id, String sn,@_SafeDeviceTypeConverter() DeviceType? deviceType,@_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt int id, String sn, DeviceType? deviceType, DeviceType? deviceTypeInfo, String? macAddress, String name, String status, bool isOnline, String firmwareVersion, String? lastOnlineAt, String? createdAt
}); });

View File

@ -30,10 +30,12 @@ Map<String, dynamic> _$DeviceTypeToJson(_DeviceType instance) =>
_DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo( _DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
sn: json['sn'] as String, sn: json['sn'] as String,
deviceType: const _SafeDeviceTypeConverter().fromJson(json['device_type']), deviceType: json['device_type'] == null
deviceTypeInfo: const _SafeDeviceTypeConverter().fromJson( ? null
json['device_type_info'], : 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?, macAddress: json['mac_address'] as String?,
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
status: json['status'] as String? ?? 'in_stock', status: json['status'] as String? ?? 'in_stock',
@ -43,23 +45,20 @@ _DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
createdAt: json['created_at'] as String?, createdAt: json['created_at'] as String?,
); );
Map<String, dynamic> _$DeviceInfoToJson( Map<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
_DeviceInfo instance, <String, dynamic>{
) => <String, dynamic>{ 'id': instance.id,
'id': instance.id, 'sn': instance.sn,
'sn': instance.sn, 'device_type': instance.deviceType,
'device_type': const _SafeDeviceTypeConverter().toJson(instance.deviceType), 'device_type_info': instance.deviceTypeInfo,
'device_type_info': const _SafeDeviceTypeConverter().toJson( 'mac_address': instance.macAddress,
instance.deviceTypeInfo, 'name': instance.name,
), 'status': instance.status,
'mac_address': instance.macAddress, 'is_online': instance.isOnline,
'name': instance.name, 'firmware_version': instance.firmwareVersion,
'status': instance.status, 'last_online_at': instance.lastOnlineAt,
'is_online': instance.isOnline, 'created_at': instance.createdAt,
'firmware_version': instance.firmwareVersion, };
'last_online_at': instance.lastOnlineAt,
'created_at': instance.createdAt,
};
_UserDevice _$UserDeviceFromJson(Map<String, dynamic> json) => _UserDevice( _UserDevice _$UserDeviceFromJson(Map<String, dynamic> json) => _UserDevice(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),

View File

@ -19,17 +19,16 @@ class DeviceController extends _$DeviceController {
); );
} }
/// null Future<bool> bindDevice(String sn, {int? spiritId}) async {
Future<String?> bindDevice(String sn, {int? spiritId}) async {
final repository = ref.read(deviceRepositoryProvider); final repository = ref.read(deviceRepositoryProvider);
final result = await repository.bindDevice(sn, spiritId: spiritId); final result = await repository.bindDevice(sn, spiritId: spiritId);
if (!ref.mounted) return '组件已卸载'; if (!ref.mounted) return false;
return result.fold( return result.fold(
(failure) => failure.message, (failure) => false,
(bindingId) { (bindingId) {
if (!ref.mounted) return '组件已卸载'; if (!ref.mounted) return false;
ref.invalidateSelf(); ref.invalidateSelf();
return null; return true;
}, },
); );
} }

View File

@ -14,7 +14,6 @@ import '../features/device/presentation/controllers/device_controller.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../widgets/glass_dialog.dart'; import '../widgets/glass_dialog.dart';
import '../theme/product_theme.dart';
/// ///
enum DeviceType { plush, badgeAi, badge } enum DeviceType { plush, badgeAi, badge }
@ -27,7 +26,6 @@ class MockDevice {
final DeviceType type; final DeviceType type;
final bool hasAI; final bool hasAI;
final bool isNetworkRequired; final bool isNetworkRequired;
final String bindStatus; // unbound / bound_by_me / bound_by_other
final BluetoothDevice? bleDevice; final BluetoothDevice? bleDevice;
const MockDevice({ const MockDevice({
@ -37,12 +35,9 @@ class MockDevice {
required this.type, required this.type,
required this.hasAI, required this.hasAI,
this.isNetworkRequired = true, this.isNetworkRequired = true,
this.bindStatus = 'unbound',
this.bleDevice, this.bleDevice,
}); });
bool get isBoundByOther => bindStatus == 'bound_by_other';
String get iconPath { String get iconPath {
switch (type) { switch (type) {
case DeviceType.plush: case DeviceType.plush:
@ -223,19 +218,10 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
}); });
// _queryDeviceByMac // _queryDeviceByMac
try { await FlutterBluePlus.startScan(
await FlutterBluePlus.startScan( timeout: const Duration(seconds: 30),
timeout: const Duration(seconds: 30), androidUsesFineLocation: true,
androidUsesFineLocation: true, );
);
} catch (e) {
// Web : requestDevice() FlutterBluePlusException
debugPrint('[BLE Scan] startScan 异常(用户可能取消了选择器): $e');
if (mounted) {
setState(() => _isSearching = false);
}
return;
}
// 30 // 30
if (mounted && _isSearching) { if (mounted && _isSearching) {
@ -257,10 +243,8 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
_macInfoCache[mac] = data; _macInfoCache[mac] = data;
final deviceTypeName = data['device_type']?['name'] as String? ?? ''; final deviceTypeName = data['device_type']?['name'] as String? ?? '';
final productCode = data['device_type']?['product_code'] as String? ?? '';
final sn = data['sn'] as String? ?? ''; final sn = data['sn'] as String? ?? '';
final isNetworkRequired = data['device_type']?['is_network_required'] as bool? ?? true; final isNetworkRequired = data['device_type']?['is_network_required'] as bool? ?? true;
final bindStatus = data['bind_status'] as String? ?? 'unbound';
final bleDevice = _pendingBleDevices[mac]; final bleDevice = _pendingBleDevices[mac];
// API // API
@ -273,10 +257,9 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
sn: sn, sn: sn,
name: displayName, name: displayName,
macAddress: mac, macAddress: mac,
type: _inferDeviceTypeByCode(productCode, displayName), type: _inferDeviceType(displayName),
hasAI: _inferHasAI(displayName), hasAI: _inferHasAI(displayName),
isNetworkRequired: isNetworkRequired, isNetworkRequired: isNetworkRequired,
bindStatus: bindStatus,
bleDevice: bleDevice, bleDevice: bleDevice,
)); ));
} }
@ -311,29 +294,15 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
} }
/// ///
/// product_code fallback DeviceType _inferDeviceType(String name) {
DeviceType _inferDeviceTypeByCode(String productCode, String name) {
// product_code
switch (productCode) {
case 'KPBL-ON':
return DeviceType.plush;
case 'DZBJ-ON':
return DeviceType.badgeAi;
case 'DZBJ-OFF':
return DeviceType.badge;
}
// fallback:
final lower = name.toLowerCase(); final lower = name.toLowerCase();
if (lower.contains('plush') || lower.contains('卡皮巴拉') || lower.contains('机芯')) { if (lower.contains('plush') || lower.contains('卡皮巴拉') || lower.contains('机芯') || lower.contains('airhub')) {
return DeviceType.plush; return DeviceType.plush;
} }
if (lower.contains('ai') || lower.contains('智能')) { if (lower.contains('ai') || lower.contains('智能')) {
return DeviceType.badgeAi; return DeviceType.badgeAi;
} }
if (lower.contains('吧唧') || lower.contains('badge')) { return DeviceType.badge;
return DeviceType.badge;
}
return DeviceType.plush;
} }
/// AI /// AI
@ -385,21 +354,6 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
bool _isConnecting = false; bool _isConnecting = false;
///
void _setThemeAndNavigate(DeviceType type) {
switch (type) {
case DeviceType.badgeAi:
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeAi);
context.go('/badge-control');
case DeviceType.badge:
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic);
context.go('/badge-basic-control');
case DeviceType.plush:
ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara);
context.go('/device-control');
}
}
/// ///
Future<void> _handleConnect() async { Future<void> _handleConnect() async {
if (_devices.isEmpty || _isConnecting) return; if (_devices.isEmpty || _isConnecting) return;
@ -411,45 +365,11 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
} }
final device = _devices[_currentIndex]; final device = _devices[_currentIndex];
debugPrint('[Bluetooth] 连接设备: sn=${device.sn}, mac=${device.macAddress}, bindStatus=${device.bindStatus}'); debugPrint('[Bluetooth] 连接设备: sn=${device.sn}, mac=${device.macAddress}, needsNetwork=${device.isNetworkRequired}');
//
if (device.isBoundByOther) {
showGlassDialog(
context: context,
title: '无法连接',
description: '该设备已被其他用户绑定,无法使用。如需解绑请联系设备所有者。',
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
if (!device.isNetworkRequired) { if (!device.isNetworkRequired) {
// -> // ->
if (device.sn.isNotEmpty) { context.go('/device-control');
setState(() => _isConnecting = true);
try {
final error = await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn);
if (!mounted) return;
if (error != null) {
setState(() => _isConnecting = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} catch (e) {
debugPrint('[Bluetooth] bindDevice 异常: $e');
}
if (!mounted) return;
setState(() => _isConnecting = false);
}
_setThemeAndNavigate(device.type);
return; return;
} }
@ -458,23 +378,11 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
debugPrint('[Bluetooth] Web 环境,直接绑定 sn=${device.sn}'); debugPrint('[Bluetooth] Web 环境,直接绑定 sn=${device.sn}');
setState(() => _isConnecting = true); setState(() => _isConnecting = true);
if (device.sn.isNotEmpty) { if (device.sn.isNotEmpty) {
final error = await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn); await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn);
if (!mounted) return;
if (error != null) {
setState(() => _isConnecting = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} }
if (!mounted) return; if (!mounted) return;
setState(() => _isConnecting = false); setState(() => _isConnecting = false);
_setThemeAndNavigate(device.type); context.go('/device-control');
return; return;
} }
@ -565,7 +473,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
children: [ children: [
// - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border // - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border
GestureDetector( GestureDetector(
onTap: () => context.pop(), onTap: () => context.go('/home'),
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
@ -830,22 +738,9 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: device.isBoundByOther color: const Color(0xFF1F2937),
? const Color(0xFF9CA3AF)
: const Color(0xFF1F2937),
), ),
), ),
//
if (device.isBoundByOther) ...[
const SizedBox(height: 4),
Text(
'已被其他用户绑定',
style: TextStyle(
fontSize: 12,
color: const Color(0xFFEF4444),
),
),
],
], ],
); );
} }
@ -903,7 +798,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
children: [ children: [
// - HTML: frosted glass with border // - HTML: frosted glass with border
GestureDetector( GestureDetector(
onTap: () => context.pop(), onTap: () => context.go('/home'),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
child: Container( child: Container(

View File

@ -19,7 +19,6 @@ import '../widgets/ios_toast.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../features/device/presentation/controllers/device_controller.dart'; import '../features/device/presentation/controllers/device_controller.dart';
import '../theme/product_theme.dart';
class DeviceControlPage extends ConsumerStatefulWidget { class DeviceControlPage extends ConsumerStatefulWidget {
const DeviceControlPage({super.key}); const DeviceControlPage({super.key});
@ -49,7 +48,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara));
_mascotAnimController = AnimationController( _mascotAnimController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(seconds: 4), duration: const Duration(seconds: 4),

View File

@ -6,7 +6,6 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../features/device/presentation/controllers/device_controller.dart'; import '../features/device/presentation/controllers/device_controller.dart';
import '../features/device/domain/entities/device.dart'; import '../features/device/domain/entities/device.dart';
import '../theme/product_theme.dart';
class ProductSelectionPage extends ConsumerStatefulWidget { class ProductSelectionPage extends ConsumerStatefulWidget {
const ProductSelectionPage({super.key}); const ProductSelectionPage({super.key});
@ -39,13 +38,6 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
'badge-basic': ['DZBJ-OFF'], 'badge-basic': ['DZBJ-OFF'],
}; };
/// ID ProductType
static const Map<String, ProductType> _productTypeMap = {
'capybara': ProductType.capybara,
'badge-ai': ProductType.badgeAi,
'badge-basic': ProductType.badgeBasic,
};
/// ///
UserDevice? _findBoundDevice(String productId, List<UserDevice> devices) { UserDevice? _findBoundDevice(String productId, List<UserDevice> devices) {
final codes = _productCodeMap[productId]; final codes = _productCodeMap[productId];
@ -198,23 +190,12 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
fadeStartY: headerHeight + 16, fadeStartY: headerHeight + 16,
fadeEndY: safeTop, fadeEndY: safeTop,
onTap: () { onTap: () {
//
final productType = _productTypeMap[product['id']] ?? ProductType.common;
ref.read(currentProductTypeProvider.notifier).set(productType);
if (boundDevice != null) { if (boundDevice != null) {
// //
final pid = product['id'] as String; context.go('/device-control');
if (pid == 'badge-ai') {
context.go('/badge-control');
} else if (pid == 'badge-basic') {
context.go('/badge-basic-control');
} else {
context.go('/device-control');
}
} else { } else {
// push //
context.push('/bluetooth'); context.go('/bluetooth');
} }
}, },
); );

View File

@ -8,8 +8,6 @@ import '../core/services/ble_provisioning_service.dart';
import '../features/device/presentation/controllers/device_controller.dart'; import '../features/device/presentation/controllers/device_controller.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../widgets/glass_dialog.dart';
import '../theme/product_theme.dart';
class WifiConfigPage extends ConsumerStatefulWidget { class WifiConfigPage extends ConsumerStatefulWidget {
final Map<String, dynamic>? extra; final Map<String, dynamic>? extra;
@ -138,36 +136,14 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
if (sn.isNotEmpty) { if (sn.isNotEmpty) {
try { try {
debugPrint('[WiFi Config] Binding device sn=$sn'); debugPrint('[WiFi Config] Binding device sn=$sn');
final error = await ref.read(deviceControllerProvider.notifier).bindDevice(sn); await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
if (!mounted) return;
if (error != null) {
setState(() => _isBinding = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} catch (e) { } catch (e) {
debugPrint('[WiFi Config] bindDevice 异常: $e'); debugPrint('[WiFi Config] bindDevice 异常: $e');
} }
} }
if (!mounted) return; if (!mounted) return;
setState(() => _isBinding = false); setState(() => _isBinding = false);
final deviceType = _deviceInfo['type'] as String? ?? ''; context.go('/device-control');
if (deviceType == 'badgeAi') {
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeAi);
context.go('/badge-control');
} else if (deviceType == 'badge') {
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic);
context.go('/badge-basic-control');
} else {
ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara);
context.go('/device-control');
}
return; return;
} }

View File

@ -1,190 +0,0 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'product_theme.g.dart';
///
enum ProductType {
///
common,
///
capybara,
/// AI
badgeAi,
///
badgeBasic,
}
///
class ProductThemeData {
///
final LinearGradient buttonGradient;
///
final Color accentColor;
///
final Color accentColorLight;
///
final List<BoxShadow> buttonShadows;
const ProductThemeData({
required this.buttonGradient,
required this.accentColor,
required this.accentColorLight,
required this.buttonShadows,
});
}
///
class ProductThemes {
ProductThemes._();
/// / (cyan blue indigo purple)
static final common = ProductThemeData(
buttonGradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0xFF22D3EE),
Color(0xFF3B82F6),
Color(0xFF6366F1),
Color(0xFF8B5CF6),
],
stops: [0.0, 0.35, 0.65, 1.0],
),
accentColor: const Color(0xFF6366F1),
accentColorLight: const Color(0xFF8B5CF6),
buttonShadows: [
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 20,
),
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.2),
offset: Offset.zero,
blurRadius: 40,
),
],
);
/// /
static final capybara = ProductThemeData(
buttonGradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFECCFA8), Color(0xFFC99672)],
),
accentColor: const Color(0xFFC99672),
accentColorLight: const Color(0xFFECCFA8),
buttonShadows: [
BoxShadow(
color: const Color(0xFFC99672).withOpacity(0.35),
offset: Offset.zero,
blurRadius: 15,
),
BoxShadow(
color: const Color(0xFFC99672).withOpacity(0.25),
offset: Offset.zero,
blurRadius: 30,
),
BoxShadow(
color: const Color(0xFFC99672).withOpacity(0.4),
offset: const Offset(0, 6),
blurRadius: 20,
),
],
);
/// AI
static final badgeAi = ProductThemeData(
buttonGradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0xFF22D3EE),
Color(0xFF60A5FA),
Color(0xFF818CF8),
Color(0xFFA78BFA),
],
stops: [0.0, 0.35, 0.70, 1.0],
),
accentColor: const Color(0xFF818CF8),
accentColorLight: const Color(0xFFA78BFA),
buttonShadows: [
BoxShadow(
color: const Color(0xFF818CF8).withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 20,
),
BoxShadow(
color: const Color(0xFFA78BFA).withOpacity(0.2),
offset: Offset.zero,
blurRadius: 40,
),
],
);
///
static final badgeBasic = ProductThemeData(
buttonGradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0xFFC084FC),
Color(0xFFD8B4FE),
Color(0xFFC4B5FD),
Color(0xFFA78BFA),
],
stops: [0.0, 0.35, 0.70, 1.0],
),
accentColor: const Color(0xFFC084FC),
accentColorLight: const Color(0xFFD8B4FE),
buttonShadows: [
BoxShadow(
color: const Color(0xFFC084FC).withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 20,
),
BoxShadow(
color: const Color(0xFFA78BFA).withOpacity(0.2),
offset: Offset.zero,
blurRadius: 40,
),
],
);
///
static ProductThemeData of(ProductType type) {
switch (type) {
case ProductType.common:
return common;
case ProductType.capybara:
return capybara;
case ProductType.badgeAi:
return badgeAi;
case ProductType.badgeBasic:
return badgeBasic;
}
}
}
/// Notifier
@Riverpod(keepAlive: true)
class CurrentProductType extends _$CurrentProductType {
@override
ProductType build() => ProductType.common;
void set(ProductType type) => state = type;
}
///
@riverpod
ProductThemeData currentProductTheme(Ref ref) {
return ProductThemes.of(ref.watch(currentProductTypeProvider));
}

View File

@ -1,121 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_theme.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Notifier
@ProviderFor(CurrentProductType)
const currentProductTypeProvider = CurrentProductTypeProvider._();
/// Notifier
final class CurrentProductTypeProvider
extends $NotifierProvider<CurrentProductType, ProductType> {
/// Notifier
const CurrentProductTypeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentProductTypeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentProductTypeHash();
@$internal
@override
CurrentProductType create() => CurrentProductType();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductType value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductType>(value),
);
}
}
String _$currentProductTypeHash() =>
r'53603ab5884787f0a4bb1aed5de18ff33089b5e7';
/// Notifier
abstract class _$CurrentProductType extends $Notifier<ProductType> {
ProductType build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<ProductType, ProductType>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ProductType, ProductType>,
ProductType,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
///
@ProviderFor(currentProductTheme)
const currentProductThemeProvider = CurrentProductThemeProvider._();
///
final class CurrentProductThemeProvider
extends
$FunctionalProvider<
ProductThemeData,
ProductThemeData,
ProductThemeData
>
with $Provider<ProductThemeData> {
///
const CurrentProductThemeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentProductThemeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentProductThemeHash();
@$internal
@override
$ProviderElement<ProductThemeData> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
ProductThemeData create(Ref ref) {
return currentProductTheme(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductThemeData value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductThemeData>(value),
);
}
}
String _$currentProductThemeHash() =>
r'a4e7be1ce8791e6e3323950346ef72e4f5d07fa9';

View File

@ -11,8 +11,8 @@ class GlassDialog extends StatelessWidget {
final String confirmText; final String confirmText;
final VoidCallback onCancel; final VoidCallback onCancel;
final VoidCallback onConfirm; final VoidCallback onConfirm;
final bool isDanger; final bool
final Gradient? confirmGradient; isDanger; // If we need a specific danger style, though CSS shows Pink Gradient default
const GlassDialog({ const GlassDialog({
super.key, super.key,
@ -24,7 +24,6 @@ class GlassDialog extends StatelessWidget {
required this.onCancel, required this.onCancel,
required this.onConfirm, required this.onConfirm,
this.isDanger = false, this.isDanger = false,
this.confirmGradient,
}); });
@override @override
@ -99,7 +98,7 @@ class GlassDialog extends StatelessWidget {
GradientButton( GradientButton(
text: confirmText, text: confirmText,
height: 48, height: 48,
gradient: confirmGradient ?? appclr.AppColors.btnPlushGradient, gradient: appclr.AppColors.btnPlushGradient,
onPressed: onConfirm, onPressed: onConfirm,
), ),
] else ...[ ] else ...[
@ -132,7 +131,7 @@ class GlassDialog extends StatelessWidget {
child: GradientButton( child: GradientButton(
text: confirmText, text: confirmText,
height: 44, height: 44,
gradient: confirmGradient ?? appclr.AppColors.btnPlushGradient, gradient: appclr.AppColors.btnPlushGradient,
onPressed: onConfirm, onPressed: onConfirm,
), ),
), ),
@ -156,7 +155,6 @@ Future<T?> showGlassDialog<T>({
String confirmText = '确定', String confirmText = '确定',
required VoidCallback onConfirm, required VoidCallback onConfirm,
bool isDanger = false, bool isDanger = false,
Gradient? confirmGradient,
}) { }) {
return showGeneralDialog<T>( return showGeneralDialog<T>(
context: context, context: context,
@ -178,7 +176,6 @@ Future<T?> showGlassDialog<T>({
onCancel: () => Navigator.of(context).pop(), onCancel: () => Navigator.of(context).pop(),
onConfirm: onConfirm, onConfirm: onConfirm,
isDanger: isDanger, isDanger: isDanger,
confirmGradient: confirmGradient,
); );
}, },
transitionBuilder: (context, anim1, anim2, child) { transitionBuilder: (context, anim1, anim2, child) {

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import '../theme/product_theme.dart';
class GradientButton extends StatelessWidget { class GradientButton extends StatelessWidget {
final String text; final String text;
@ -10,7 +9,6 @@ class GradientButton extends StatelessWidget {
final double height; final double height;
final bool isLoading; final bool isLoading;
final Gradient? gradient; final Gradient? gradient;
final List<BoxShadow>? shadows;
const GradientButton({ const GradientButton({
super.key, super.key,
@ -20,53 +18,57 @@ class GradientButton extends StatelessWidget {
this.height = 48.0, // this.height = 48.0, //
this.isLoading = false, this.isLoading = false,
this.gradient, this.gradient,
this.shadows,
}); });
/// ProductThemeData // Check if using plush/capybara gradient
factory GradientButton.fromTheme({ bool get _isPlushGradient {
Key? key, if (gradient == null) return false;
required String text, if (gradient is LinearGradient) {
required ProductThemeData theme, final lg = gradient as LinearGradient;
VoidCallback? onPressed, // Check if colors match plush gradient colors
double width = double.infinity, if (lg.colors.length >= 2) {
double height = 48.0, return lg.colors.first.value == 0xFFECCFA8 ||
bool isLoading = false, lg.colors.last.value == 0xFFC99672;
}) { }
return GradientButton( }
key: key, return false;
text: text,
onPressed: onPressed,
width: width,
height: height,
isLoading: isLoading,
gradient: theme.buttonGradient,
shadows: theme.buttonShadows,
);
} }
List<BoxShadow> get _boxShadows { List<BoxShadow> get _boxShadows {
if (shadows != null) return shadows!; if (_isPlushGradient) {
// // Warm brown glow for Capybara plush gradient
if (gradient is LinearGradient) { return [
final lg = gradient as LinearGradient; BoxShadow(
if (lg.colors.length >= 2) { color: const Color(0xFFC99672).withOpacity(0.35),
final shadowColor = lg.colors[lg.colors.length ~/ 2]; offset: Offset.zero,
return [ blurRadius: 15,
BoxShadow( ),
color: shadowColor.withOpacity(0.4), BoxShadow(
offset: const Offset(0, 4), color: const Color(0xFFC99672).withOpacity(0.25),
blurRadius: 20, offset: Offset.zero,
), blurRadius: 30,
BoxShadow( ),
color: lg.colors.last.withOpacity(0.2), BoxShadow(
offset: Offset.zero, color: const Color(0xFFC99672).withOpacity(0.4),
blurRadius: 40, offset: const Offset(0, 6),
), blurRadius: 20,
]; ),
} ];
} else {
// Purple/indigo glow for primary gradient
return [
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 20,
),
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.2),
offset: Offset.zero,
blurRadius: 40,
),
];
} }
return AppColors.shadowPrimaryButton;
} }
@override @override

View File

@ -40,14 +40,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.10" version: "0.13.10"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -588,14 +580,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: "direct main"
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -972,14 +956,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.2" version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:

View File

@ -65,7 +65,6 @@ dependencies:
flutter_blue_plus: ^1.31.0 flutter_blue_plus: ^1.31.0
flutter_svg: ^2.0.9 flutter_svg: ^2.0.9
image_picker: ^1.2.1 image_picker: ^1.2.1
image: ^4.3.0
just_audio: ^0.9.42 just_audio: ^0.9.42
http: ^1.2.0 http: ^1.2.0
video_player: ^2.9.2 video_player: ^2.9.2