- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化 - 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失) - 书架页: bookPop弹出+粒子效果(三段式动画完整链路) - 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局 - 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化 - 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌 - 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面 - 阶段总结文档更新 Co-authored-by: Cursor <cursoragent@cursor.com>
628 lines
17 KiB
Dart
628 lines
17 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import '../widgets/animated_gradient_background.dart';
|
|
import '../widgets/gradient_button.dart';
|
|
|
|
class WifiConfigPage extends StatefulWidget {
|
|
const WifiConfigPage({super.key});
|
|
|
|
@override
|
|
State<WifiConfigPage> createState() => _WifiConfigPageState();
|
|
}
|
|
|
|
class _WifiConfigPageState extends State<WifiConfigPage>
|
|
with TickerProviderStateMixin {
|
|
int _currentStep = 1;
|
|
String _selectedWifiSsid = '';
|
|
final TextEditingController _passwordController = TextEditingController();
|
|
bool _obscurePassword = true;
|
|
|
|
// Progress State
|
|
double _progress = 0.0;
|
|
String _progressText = '正在连接WiFi...';
|
|
|
|
// Device Info (Mock or from Route Args)
|
|
// We'll try to get it from arguments, default to a fallback
|
|
Map<String, dynamic> _deviceInfo = {};
|
|
|
|
// Mock WiFi List
|
|
final List<Map<String, dynamic>> _wifiList = [
|
|
{'ssid': 'Home_5G', 'level': 4},
|
|
{'ssid': 'Office_WiFi', 'level': 3},
|
|
{'ssid': 'Guest_Network', 'level': 2},
|
|
];
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// Retrieve device info from arguments
|
|
final args = ModalRoute.of(context)?.settings.arguments;
|
|
if (args is Map<String, dynamic>) {
|
|
_deviceInfo = args;
|
|
}
|
|
}
|
|
|
|
void _handleNext() {
|
|
if (_currentStep == 1 && _selectedWifiSsid.isEmpty) return;
|
|
if (_currentStep == 2 && _passwordController.text.isEmpty) return;
|
|
|
|
if (_currentStep == 4) {
|
|
// Navigate to Device Control
|
|
// Use pushNamedAndRemoveUntil to remove Bluetooth and WiFi pages from stack
|
|
// but keep Home page so back button goes to Home
|
|
context.go('/device-control');
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_currentStep++;
|
|
});
|
|
|
|
if (_currentStep == 3) {
|
|
_startConnecting();
|
|
}
|
|
}
|
|
|
|
void _handleBack() {
|
|
if (_currentStep > 1) {
|
|
setState(() {
|
|
_currentStep--;
|
|
});
|
|
} else {
|
|
context.go('/home');
|
|
}
|
|
}
|
|
|
|
void _startConnecting() {
|
|
const steps = [
|
|
{'progress': 0.3, 'text': '正在连接WiFi...'},
|
|
{'progress': 0.6, 'text': '正在验证密码...'},
|
|
{'progress': 0.9, 'text': '正在同步设备...'},
|
|
{'progress': 1.0, 'text': '完成!'},
|
|
];
|
|
|
|
int stepIndex = 0;
|
|
Timer.periodic(const Duration(milliseconds: 800), (timer) {
|
|
if (stepIndex < steps.length) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_progress = steps[stepIndex]['progress'] as double;
|
|
_progressText = steps[stepIndex]['text'] as String;
|
|
});
|
|
}
|
|
stepIndex++;
|
|
} else {
|
|
timer.cancel();
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentStep = 4;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
resizeToAvoidBottomInset: true,
|
|
body: Stack(
|
|
children: [
|
|
// Background
|
|
_buildGradientBackground(),
|
|
|
|
Positioned.fill(
|
|
child: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
// Header
|
|
_buildHeader(),
|
|
|
|
// Content
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Column(
|
|
children: [
|
|
// Steps Indicator
|
|
_buildStepIndicator(),
|
|
const SizedBox(height: 32),
|
|
|
|
// Dynamic Step Content
|
|
_buildCurrentStepContent(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Footer
|
|
_buildFooter(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Common Gradient Background
|
|
Widget _buildGradientBackground() {
|
|
return const AnimatedGradientBackground();
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
child: Row(
|
|
children: [
|
|
// Back button - HTML: bg rgba(255,255,255,0.6), border-radius: 12px, color #4B5563
|
|
GestureDetector(
|
|
onTap: _handleBack,
|
|
child: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: Colors.white.withOpacity(0.6),
|
|
),
|
|
child: const Icon(
|
|
Icons.arrow_back_ios_new,
|
|
size: 18,
|
|
color: Color(0xFF4B5563), // Gray per HTML, not purple
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
'WiFi配网',
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 48), // Balance back button
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStepIndicator() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(4, (index) {
|
|
int step = index + 1;
|
|
bool isActive = step == _currentStep;
|
|
bool isCompleted = step < _currentStep;
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: isActive ? 24 : 8,
|
|
height: 8,
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
decoration: BoxDecoration(
|
|
color: isCompleted
|
|
? const Color(0xFF22C55E) // Green for completed
|
|
: isActive
|
|
? const Color(0xFF8B5CF6) // Purple for active
|
|
: const Color(0xFF8B5CF6).withOpacity(0.3), // Faded purple
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
Widget _buildCurrentStepContent() {
|
|
switch (_currentStep) {
|
|
case 1:
|
|
return _buildStep1();
|
|
case 2:
|
|
return _buildStep2();
|
|
case 3:
|
|
return _buildStep3();
|
|
case 4:
|
|
return _buildStep4();
|
|
default:
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
// Step 1: Select Network
|
|
Widget _buildStep1() {
|
|
return Column(
|
|
children: [
|
|
// Icon
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 24),
|
|
child: const Icon(Icons.wifi, size: 80, color: Color(0xFF8B5CF6)),
|
|
),
|
|
Text(
|
|
'选择WiFi网络',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'设备需要连接WiFi以使用AI功能',
|
|
style: TextStyle(
|
|
|
|
fontSize: 14,
|
|
color: Color(0xFF6B7280),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// List
|
|
Column(
|
|
children: _wifiList.map((wifi) => _buildWifiItem(wifi)).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildWifiItem(Map<String, dynamic> wifi) {
|
|
bool isSelected = _selectedWifiSsid == wifi['ssid'];
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setState(() => _selectedWifiSsid = wifi['ssid']);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.8),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? const Color(0xFF8B5CF6)
|
|
: Colors.white.withOpacity(0.5),
|
|
width: isSelected ? 2 : 1,
|
|
),
|
|
boxShadow: isSelected
|
|
? [
|
|
BoxShadow(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.2),
|
|
blurRadius: 0,
|
|
spreadRadius: 2,
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
wifi['ssid'],
|
|
style: const TextStyle(
|
|
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1F2937),
|
|
),
|
|
),
|
|
),
|
|
// HTML uses per-level SVG icons: wifi-1.svg to wifi-4.svg
|
|
Opacity(
|
|
opacity: 0.8,
|
|
child: SvgPicture.asset(
|
|
'assets/www/icons/wifi-${wifi['level']}.svg',
|
|
width: 24,
|
|
height: 24,
|
|
colorFilter: const ColorFilter.mode(
|
|
Color(0xFF6B7280),
|
|
BlendMode.srcIn,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Step 2: Enter Password
|
|
Widget _buildStep2() {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
margin: const EdgeInsets.only(bottom: 24),
|
|
child: const Icon(
|
|
Icons.lock_outline,
|
|
size: 80,
|
|
color: Color(0xFF8B5CF6),
|
|
),
|
|
),
|
|
Text(
|
|
_selectedWifiSsid,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'请输入WiFi密码',
|
|
style: TextStyle(
|
|
|
|
fontSize: 14,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
TextField(
|
|
controller: _passwordController,
|
|
obscureText: _obscurePassword,
|
|
onChanged: (_) => setState(() {}),
|
|
decoration: InputDecoration(
|
|
hintText: '输入密码',
|
|
filled: true,
|
|
fillColor: Colors.white.withOpacity(0.8),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.all(20),
|
|
suffixIcon: Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
|
color: const Color(0xFF9CA3AF),
|
|
size: 22,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
_obscurePassword = !_obscurePassword;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
style: const TextStyle(fontSize: 16),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Step 3: Connecting
|
|
Widget _buildStep3() {
|
|
return Column(
|
|
children: [
|
|
// Animation placeholder (using Icon for now, can be upgraded to Wave animation)
|
|
SizedBox(
|
|
height: 120,
|
|
child: Center(
|
|
child: TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0, end: 1),
|
|
duration: const Duration(seconds: 1),
|
|
builder: (context, value, child) {
|
|
return Icon(
|
|
Icons.wifi_tethering,
|
|
size: 80 + (value * 10),
|
|
color: const Color(0xFF8B5CF6).withOpacity(1 - value * 0.5),
|
|
);
|
|
},
|
|
onEnd:
|
|
() {}, // Repeat logic usually handled by AnimationController
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'正在配网...',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// Progress Bar
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(3),
|
|
child: SizedBox(
|
|
height: 6,
|
|
child: LinearProgressIndicator(
|
|
value: _progress,
|
|
backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.2),
|
|
valueColor: const AlwaysStoppedAnimation(Color(0xFF8B5CF6)),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_progressText,
|
|
style: TextStyle(
|
|
|
|
fontSize: 14,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Get device icon path based on device type
|
|
String _getDeviceIconPath() {
|
|
final type = _deviceInfo['type'] as String? ?? 'plush';
|
|
switch (type) {
|
|
case 'plush_core':
|
|
case 'plush':
|
|
return 'assets/www/icons/pixel-capybara.svg';
|
|
case 'badge_ai':
|
|
return 'assets/www/icons/pixel-badge-ai.svg';
|
|
case 'badge_basic':
|
|
case 'badge':
|
|
return 'assets/www/icons/pixel-badge-basic.svg';
|
|
default:
|
|
return 'assets/www/icons/pixel-capybara.svg';
|
|
}
|
|
}
|
|
|
|
// Step 4: Result (Success) - centered vertically
|
|
Widget _buildStep4() {
|
|
return Column(
|
|
children: [
|
|
const SizedBox(height: 80),
|
|
// Success Icon Stack - HTML: no white background
|
|
Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// Device icon container - 120x120 per HTML
|
|
SizedBox(
|
|
width: 120,
|
|
height: 120,
|
|
child: SvgPicture.asset(
|
|
_getDeviceIconPath(),
|
|
width: 120,
|
|
height: 120,
|
|
placeholderBuilder: (_) => const Icon(
|
|
Icons.smart_toy,
|
|
size: 80,
|
|
color: Color(0xFF8B5CF6),
|
|
),
|
|
),
|
|
),
|
|
// Check badge
|
|
Positioned(
|
|
bottom: -5,
|
|
right: -5,
|
|
child: Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF22C55E),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white, width: 3),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF22C55E).withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(Icons.check, color: Colors.white, size: 18),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'配网成功!',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color(0xFF1F2937),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'设备已成功连接到网络',
|
|
style: TextStyle(
|
|
|
|
fontSize: 14,
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFooter() {
|
|
bool showNext = false;
|
|
String nextText = '下一步';
|
|
|
|
if (_currentStep == 1 && _selectedWifiSsid.isNotEmpty) showNext = true;
|
|
if (_currentStep == 2 && _passwordController.text.isNotEmpty) {
|
|
showNext = true;
|
|
nextText = '连接';
|
|
}
|
|
if (_currentStep == 4) {
|
|
showNext = true;
|
|
nextText = '进入设备';
|
|
}
|
|
|
|
if (!showNext && _currentStep != 3) {
|
|
// Show cancel only?
|
|
return Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: TextButton(
|
|
onPressed: () => context.go('/bluetooth'),
|
|
child: Text(
|
|
'取消',
|
|
style: TextStyle(
|
|
|
|
color: const Color(0xFF6B7280),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_currentStep == 3)
|
|
return const SizedBox(height: 100); // Hide buttons during connection
|
|
|
|
return Container(
|
|
padding: EdgeInsets.fromLTRB(
|
|
32,
|
|
20,
|
|
32,
|
|
MediaQuery.of(context).padding.bottom + 40,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Cancel button - frosted glass style
|
|
if (_currentStep < 4)
|
|
GestureDetector(
|
|
onTap: _handleBack,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 14,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.8),
|
|
borderRadius: BorderRadius.circular(25),
|
|
border: Border.all(color: const Color(0xFFE5E7EB)),
|
|
),
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF6B7280),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (_currentStep < 4) const SizedBox(width: 16),
|
|
|
|
// Constrained button (not full-width)
|
|
GradientButton(
|
|
text: nextText,
|
|
onPressed: _handleNext,
|
|
height: 56,
|
|
width: _currentStep == 4 ? 200 : 160,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|