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

View File

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

View File

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

View File

@ -1,14 +1,6 @@
import 'package:flutter/foundation.dart' show kIsWeb;
class ApiConfig {
/// Web
static const String _localUrl = '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;
///
static const String baseUrl = 'http://192.168.124.8:8000';
/// HTTPS
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:riverpod_annotation/riverpod_annotation.dart';
@ -11,23 +9,12 @@ import '../../pages/product_selection_page.dart';
import '../../pages/profile/profile_page.dart';
import '../../pages/webview_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_transfer_page.dart';
import '../../features/device/data/datasources/device_remote_data_source.dart';
import '../../theme/product_theme.dart';
import '../network/token_manager.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
GoRouter goRouter(Ref ref) {
final tokenManager = ref.watch(tokenManagerProvider);
@ -43,37 +30,6 @@ GoRouter goRouter(Ref ref) {
return '/login';
}
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 null;
@ -107,14 +63,6 @@ GoRouter goRouter(Ref ref) {
path: '/webview_fallback',
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(
path: '/badge',
builder: (context, state) => const BadgeHomePage(),
@ -125,7 +73,6 @@ GoRouter goRouter(Ref ref) {
final extra = state.extra as Map<String, dynamic>? ?? {};
return BadgeTransferPage(
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);
debugPrint('[Login] tokenLogin 结果: $success');
if (success && mounted) {
context.go('/login');
await _navigateAfterLogin();
}
}
@ -274,8 +274,25 @@ class _LoginPageState extends ConsumerState<LoginPage> {
.read(authControllerProvider.notifier)
.codeLogin(_phoneController.text, _codeController.text);
if (success && mounted) {
// /login router redirect
context.go('/login');
await _navigateAfterLogin();
}
}
Future<void> _navigateAfterLogin() async {
if (!mounted) return;
try {
final devices = await ref.read(deviceControllerProvider.future);
if (!mounted) return;
if (devices.isNotEmpty) {
debugPrint('[Login] User has ${devices.length} device(s), navigating to device control');
context.go('/device-control');
} else {
debugPrint('[Login] No devices, navigating to home');
context.go('/home');
}
} catch (e) {
debugPrint('[Login] Device check failed: $e');
if (mounted) context.go('/home');
}
}

View File

@ -1,361 +1,38 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.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
/// 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;
enum TransferState { idle, transferring, complete, error }
/// MTU = 512 ble_service_config.h
/// requestMtu iOS/Web
static const _defaultMtu = 512;
class BadgeTransferState {
final TransferState transferState;
final List<ScanResult> scannedDevices;
final BluetoothDevice? selectedDevice;
final double progress;
final String? errorMessage;
/// ATT 3 + GATT Handle 2
static const _attOverhead = 3;
/// : (1) + (1)
static const _frameHeaderSize = 2;
/// malloc / fopen
static const _preambleDelayMs = 50;
/// (ms) ESP32 BLE
static const _packetDelayMs = 5;
/// N write-with-response
static int get _syncInterval => kIsWeb ? 5 : 10;
///
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;
}
const BadgeTransferState({
this.transferState = TransferState.idle,
this.scannedDevices = const [],
this.selectedDevice,
this.progress = 0,
this.errorMessage,
});
// 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)'),
BadgeTransferState copyWith({
TransferState? transferState,
List<ScanResult>? scannedDevices,
BluetoothDevice? selectedDevice,
double? progress,
String? errorMessage,
}) {
return BadgeTransferState(
transferState: transferState ?? this.transferState,
scannedDevices: scannedDevices ?? this.scannedDevices,
selectedDevice: selectedDevice ?? this.selectedDevice,
progress: progress ?? this.progress,
errorMessage: errorMessage ?? this.errorMessage,
);
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:riverpod_annotation/riverpod_annotation.dart';
import '../../data/services/badge_transfer_service.dart';
part 'badge_transfer_controller.g.dart';
enum TransferStatus { idle, scanning, connecting, transferring, done, error }
@ -13,14 +9,12 @@ class BadgeTransferState {
final TransferStatus status;
final BluetoothDevice? device;
final double progress;
final String? statusMessage;
final String? errorMessage;
const BadgeTransferState({
this.status = TransferStatus.idle,
this.device,
this.progress = 0.0,
this.statusMessage,
this.errorMessage,
});
@ -28,14 +22,12 @@ class BadgeTransferState {
TransferStatus? status,
BluetoothDevice? device,
double? progress,
String? statusMessage,
String? errorMessage,
}) {
return BadgeTransferState(
status: status ?? this.status,
device: device ?? this.device,
progress: progress ?? this.progress,
statusMessage: statusMessage ?? this.statusMessage,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@ -43,8 +35,6 @@ class BadgeTransferState {
@riverpod
class BadgeTransferController extends _$BadgeTransferController {
final _transferService = BadgeBleTransferService();
@override
BadgeTransferState build() {
ref.onDispose(() {
@ -53,50 +43,53 @@ class BadgeTransferController extends _$BadgeTransferController {
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(
BluetoothDevice device,
String imageUrl, {
Uint8List? imageBytes,
}) async {
String imageUrl,
) async {
if (!ref.mounted) return;
state = state.copyWith(
status: TransferStatus.connecting,
device: device,
progress: 0.0,
statusMessage: '正在连接设备...',
);
try {
await _transferService.connectAndTransfer(
device: device,
imageUrl: imageUrl,
imageBytes: imageBytes,
onProgress: (progress, message) {
await device.connect();
if (!ref.mounted) return;
final newStatus = progress < 0.15
? TransferStatus.connecting
: TransferStatus.transferring;
state = state.copyWith(
status: newStatus,
progress: progress,
statusMessage: message,
);
},
);
state = state.copyWith(status: TransferStatus.transferring);
// Transfer logic here
await Future.delayed(const Duration(seconds: 2));
if (!ref.mounted) return;
state = state.copyWith(
status: TransferStatus.done,
progress: 1.0,
statusMessage: '传输完成',
);
} catch (e) {
debugPrint('[BadgeTransfer] 传输失败: $e');
if (!ref.mounted) return;
state = state.copyWith(
status: TransferStatus.error,
errorMessage: e.toString().replaceFirst('Exception: ', ''),
errorMessage: e.toString(),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,178 +1,79 @@
import 'dart:math';
import 'package:flutter/material.dart';
///
class TransferProgressRing extends StatefulWidget {
class TransferProgressRing extends StatelessWidget {
final double progress;
final bool isComplete;
final Color accentColor;
const TransferProgressRing({
super.key,
required this.progress,
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
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _progressAnim,
builder: (context, child) {
final p = _progressAnim.value.clamp(0.0, 1.0);
return SizedBox(
width: 120,
height: 120,
child: CustomPaint(
painter: _GradientRingPainter(
progress: p,
isComplete: widget.isComplete,
accentColor: widget.accentColor,
painter: _ProgressRingPainter(
progress: progress,
isComplete: isComplete,
),
child: Center(
child: widget.isComplete
? TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
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),
),
)
child: isComplete
? const Icon(Icons.check, color: Color(0xFF10B981), size: 48)
: Text(
'${(p * 100).toInt()}%',
style: TextStyle(
'${(progress * 100).toInt()}%',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: widget.accentColor,
color: Color(0xFF8B5CF6),
),
),
),
),
);
},
);
}
}
class _GradientRingPainter extends CustomPainter {
class _ProgressRingPainter extends CustomPainter {
final double progress;
final bool isComplete;
final Color accentColor;
_GradientRingPainter({
required this.progress,
required this.isComplete,
required this.accentColor,
});
_ProgressRingPainter({required this.progress, required this.isComplete});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 6;
final rect = Rect.fromCircle(center: center, radius: radius);
//
// Background ring
final bgPaint = Paint()
..color = accentColor.withOpacity(0.1)
..color = const Color(0xFFE5E7EB)
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
if (progress <= 0) return;
//
final sweepAngle = 2 * pi * progress;
if (isComplete) {
final completePaint = Paint()
..color = const Color(0xFF10B981)
// Progress arc
final color = isComplete ? const Color(0xFF10B981) : const Color(0xFF8B5CF6);
final fgPaint = Paint()
..color = color
..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),
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2,
2 * pi * progress,
false,
fgPaint,
);
final gradientPaint = Paint()
..shader = gradient.createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, -pi / 2, sweepAngle, false, gradientPaint);
}
@override
bool shouldRepaint(covariant _GradientRingPainter oldDelegate) =>
oldDelegate.progress != progress ||
oldDelegate.isComplete != isComplete ||
oldDelegate.accentColor != accentColor;
bool shouldRepaint(covariant _ProgressRingPainter oldDelegate) =>
oldDelegate.progress != progress || oldDelegate.isComplete != isComplete;
}

View File

@ -3,23 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'device.freezed.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
abstract class DeviceType with _$DeviceType {
const factory DeviceType({
@ -41,8 +24,8 @@ abstract class DeviceInfo with _$DeviceInfo {
const factory DeviceInfo({
required int id,
required String sn,
@_SafeDeviceTypeConverter() DeviceType? deviceType,
@_SafeDeviceTypeConverter() DeviceType? deviceTypeInfo,
DeviceType? deviceType,
DeviceType? deviceTypeInfo,
String? macAddress,
@Default('') String name,
@Default('in_stock') String status,

View File

@ -296,7 +296,7 @@ as String?,
/// @nodoc
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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -329,7 +329,7 @@ abstract mixin class $DeviceInfoCopyWith<$Res> {
factory $DeviceInfoCopyWith(DeviceInfo value, $Res Function(DeviceInfo) _then) = _$DeviceInfoCopyWithImpl;
@useResult
$Res call({
int id, String sn,@_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) {
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 _:
@ -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) {
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 _:
@ -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) {
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 _:
@ -524,13 +524,13 @@ return $default(_that.id,_that.sn,_that.deviceType,_that.deviceTypeInfo,_that.ma
@JsonSerializable()
class _DeviceInfo implements DeviceInfo {
const _DeviceInfo({required this.id, required this.sn, @_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);
@override final int id;
@override final String sn;
@override@_SafeDeviceTypeConverter() final DeviceType? deviceType;
@override@_SafeDeviceTypeConverter() final DeviceType? deviceTypeInfo;
@override final DeviceType? deviceType;
@override final DeviceType? deviceTypeInfo;
@override final String? macAddress;
@override@JsonKey() final String name;
@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;
@override @useResult
$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(
id: (json['id'] as num).toInt(),
sn: json['sn'] as String,
deviceType: const _SafeDeviceTypeConverter().fromJson(json['device_type']),
deviceTypeInfo: const _SafeDeviceTypeConverter().fromJson(
json['device_type_info'],
),
deviceType: json['device_type'] == null
? null
: DeviceType.fromJson(json['device_type'] as Map<String, dynamic>),
deviceTypeInfo: json['device_type_info'] == null
? null
: DeviceType.fromJson(json['device_type_info'] as Map<String, dynamic>),
macAddress: json['mac_address'] as String?,
name: json['name'] as String? ?? '',
status: json['status'] as String? ?? 'in_stock',
@ -43,15 +45,12 @@ _DeviceInfo _$DeviceInfoFromJson(Map<String, dynamic> json) => _DeviceInfo(
createdAt: json['created_at'] as String?,
);
Map<String, dynamic> _$DeviceInfoToJson(
_DeviceInfo instance,
) => <String, dynamic>{
Map<String, dynamic> _$DeviceInfoToJson(_DeviceInfo instance) =>
<String, dynamic>{
'id': instance.id,
'sn': instance.sn,
'device_type': const _SafeDeviceTypeConverter().toJson(instance.deviceType),
'device_type_info': const _SafeDeviceTypeConverter().toJson(
instance.deviceTypeInfo,
),
'device_type': instance.deviceType,
'device_type_info': instance.deviceTypeInfo,
'mac_address': instance.macAddress,
'name': instance.name,
'status': instance.status,
@ -59,7 +58,7 @@ Map<String, dynamic> _$DeviceInfoToJson(
'firmware_version': instance.firmwareVersion,
'last_online_at': instance.lastOnlineAt,
'created_at': instance.createdAt,
};
};
_UserDevice _$UserDeviceFromJson(Map<String, dynamic> json) => _UserDevice(
id: (json['id'] as num).toInt(),

View File

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

View File

@ -19,7 +19,6 @@ import '../widgets/ios_toast.dart';
import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart';
import '../features/device/presentation/controllers/device_controller.dart';
import '../theme/product_theme.dart';
class DeviceControlPage extends ConsumerStatefulWidget {
const DeviceControlPage({super.key});
@ -49,7 +48,6 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
@override
void initState() {
super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara));
_mascotAnimController = AnimationController(
vsync: this,
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 '../features/device/presentation/controllers/device_controller.dart';
import '../features/device/domain/entities/device.dart';
import '../theme/product_theme.dart';
class ProductSelectionPage extends ConsumerStatefulWidget {
const ProductSelectionPage({super.key});
@ -39,13 +38,6 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
'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) {
final codes = _productCodeMap[productId];
@ -198,23 +190,12 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
fadeStartY: headerHeight + 16,
fadeEndY: safeTop,
onTap: () {
//
final productType = _productTypeMap[product['id']] ?? ProductType.common;
ref.read(currentProductTypeProvider.notifier).set(productType);
if (boundDevice != null) {
//
final pid = product['id'] as String;
if (pid == 'badge-ai') {
context.go('/badge-control');
} else if (pid == 'badge-basic') {
context.go('/badge-basic-control');
} else {
//
context.go('/device-control');
}
} 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 '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart';
import '../widgets/glass_dialog.dart';
import '../theme/product_theme.dart';
class WifiConfigPage extends ConsumerStatefulWidget {
final Map<String, dynamic>? extra;
@ -138,36 +136,14 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
if (sn.isNotEmpty) {
try {
debugPrint('[WiFi Config] Binding device sn=$sn');
final error = 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;
}
await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
} catch (e) {
debugPrint('[WiFi Config] bindDevice 异常: $e');
}
}
if (!mounted) return;
setState(() => _isBinding = false);
final deviceType = _deviceInfo['type'] as String? ?? '';
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;
}

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

View File

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

View File

@ -40,14 +40,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.10"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
@ -588,14 +580,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -972,14 +956,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
pub_semver:
dependency: transitive
description:

View File

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