diff --git a/airhub_app/assets/fonts/Inter-Bold.ttf b/airhub_app/assets/fonts/Inter-Bold.ttf
new file mode 100644
index 0000000..9fb9b75
Binary files /dev/null and b/airhub_app/assets/fonts/Inter-Bold.ttf differ
diff --git a/airhub_app/assets/fonts/Inter-Medium.ttf b/airhub_app/assets/fonts/Inter-Medium.ttf
new file mode 100644
index 0000000..458cd06
Binary files /dev/null and b/airhub_app/assets/fonts/Inter-Medium.ttf differ
diff --git a/airhub_app/assets/fonts/Inter-Regular.ttf b/airhub_app/assets/fonts/Inter-Regular.ttf
new file mode 100644
index 0000000..b7aaca8
Binary files /dev/null and b/airhub_app/assets/fonts/Inter-Regular.ttf differ
diff --git a/airhub_app/assets/fonts/Inter-SemiBold.ttf b/airhub_app/assets/fonts/Inter-SemiBold.ttf
new file mode 100644
index 0000000..47f8ab1
Binary files /dev/null and b/airhub_app/assets/fonts/Inter-SemiBold.ttf differ
diff --git a/airhub_app/assets/www/icons/pixel-badge-ai.svg b/airhub_app/assets/www/icons/pixel-badge-ai.svg
index b287fb9..94858d6 100644
--- a/airhub_app/assets/www/icons/pixel-badge-ai.svg
+++ b/airhub_app/assets/www/icons/pixel-badge-ai.svg
@@ -1,32 +1,27 @@
diff --git a/airhub_app/assets/www/icons/pixel-badge-basic.svg b/airhub_app/assets/www/icons/pixel-badge-basic.svg
index 7445837..e75ec53 100644
--- a/airhub_app/assets/www/icons/pixel-badge-basic.svg
+++ b/airhub_app/assets/www/icons/pixel-badge-basic.svg
@@ -1,28 +1,23 @@
diff --git a/airhub_app/assets/www/icons/pixel-capybara.svg b/airhub_app/assets/www/icons/pixel-capybara.svg
index 4285867..2fa411b 100644
--- a/airhub_app/assets/www/icons/pixel-capybara.svg
+++ b/airhub_app/assets/www/icons/pixel-capybara.svg
@@ -1,39 +1,33 @@
diff --git a/airhub_app/assets/www/icons/pixel-mystery-box.svg b/airhub_app/assets/www/icons/pixel-mystery-box.svg
index 46f487e..11d19d1 100644
--- a/airhub_app/assets/www/icons/pixel-mystery-box.svg
+++ b/airhub_app/assets/www/icons/pixel-mystery-box.svg
@@ -1,38 +1,32 @@
diff --git a/airhub_app/fix_svg_styles.py b/airhub_app/fix_svg_styles.py
new file mode 100644
index 0000000..6886a27
--- /dev/null
+++ b/airhub_app/fix_svg_styles.py
@@ -0,0 +1,75 @@
+
+import os
+import re
+
+def fix_svg_file(filepath):
+ with open(filepath, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Find style block
+ style_match = re.search(r'', content, re.DOTALL)
+ if not style_match:
+ print(f"Skipping {os.path.basename(filepath)}: No style block found")
+ return False
+
+ style_content = style_match.group(1)
+
+ # Parse class -> fill mappings
+ # Matches .classname { fill: #color; }
+ # Also handles formatting variations
+ mappings = {}
+
+ # Regex for class definition: .name { ... }
+ # We look for fill: ... inside
+ class_pattern = re.compile(r'\.([\w-]+)\s*\{([^}]+)\}')
+
+ for match in class_pattern.finditer(style_content):
+ class_name = match.group(1)
+ body = match.group(2)
+
+ # Extract fill color
+ fill_match = re.search(r'fill:\s*(#[0-9a-fA-F]{3,6})', body)
+ if fill_match:
+ mappings[class_name] = fill_match.group(1)
+
+ if not mappings:
+ print(f"Skipping {os.path.basename(filepath)}: No fill mappings found in style")
+ return False
+
+ # Remove style block
+ new_content = re.sub(r'', '', content, flags=re.DOTALL)
+
+ # Replace class="name" with fill="color"
+ # Note: We keep other attributes. If class is the only one, we replace it.
+ # If other attributes exist, we should append fill and remove class?
+ # Simplest approach: Replace `class="name"` with `fill="color"`
+
+ changed = False
+ for cls, color in mappings.items():
+ # Match class="name" or class='name'
+ # Be careful not to replace partial matches (e.g. class="name-suffix")
+ pattern = re.compile(r'class=["\']' + re.escape(cls) + r'["\']')
+ if pattern.search(new_content):
+ new_content = pattern.sub(f'fill="{color}"', new_content)
+ changed = True
+
+ if changed:
+ with open(filepath, 'w', encoding='utf-8') as f:
+ f.write(new_content)
+ print(f"Fixed {os.path.basename(filepath)}")
+ return True
+ else:
+ print(f"No class usages found for {os.path.basename(filepath)}")
+ return False
+
+def main():
+ target_dir = '/Users/maidong/Desktop/zyc/qiyuan_gitea/rtc_prd/airhub_app/assets/www/icons'
+ count = 0
+ for filename in os.listdir(target_dir):
+ if filename.endswith('.svg'):
+ if fix_svg_file(os.path.join(target_dir, filename)):
+ count += 1
+ print(f"Total files fixed: {count}")
+
+if __name__ == '__main__':
+ main()
diff --git a/airhub_app/ios/Podfile.lock b/airhub_app/ios/Podfile.lock
index 193edb5..6968536 100644
--- a/airhub_app/ios/Podfile.lock
+++ b/airhub_app/ios/Podfile.lock
@@ -1,5 +1,10 @@
PODS:
- Flutter (1.0.0)
+ - flutter_blue_plus_darwin (0.0.2):
+ - Flutter
+ - FlutterMacOS
+ - image_picker_ios (0.0.1):
+ - Flutter
- permission_handler_apple (9.3.0):
- Flutter
- webview_flutter_wkwebview (0.0.1):
@@ -8,12 +13,18 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
+ - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
+ flutter_blue_plus_darwin:
+ :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
webview_flutter_wkwebview:
@@ -21,6 +32,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
+ image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
diff --git a/airhub_app/lib/main.dart b/airhub_app/lib/main.dart
index 8d461c6..ce3a144 100644
--- a/airhub_app/lib/main.dart
+++ b/airhub_app/lib/main.dart
@@ -1,8 +1,14 @@
import 'package:flutter/material.dart';
import 'pages/login_page.dart';
import 'pages/webview_page.dart';
+import 'pages/home_page.dart';
+import 'pages/bluetooth_page.dart';
+import 'pages/wifi_config_page.dart';
+import 'pages/device_control_page.dart';
import 'theme/app_theme.dart';
+import 'pages/profile/profile_page.dart'; // Import ProfilePage
+
void main() {
runApp(const AirhubApp());
}
@@ -17,11 +23,17 @@ class AirhubApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
// Initial Route
- home: const LoginPage(),
+ home: const DeviceControlPage(),
// Named Routes
routes: {
'/login': (context) => const LoginPage(),
- '/home': (context) => const WebViewPage(),
+ '/home': (context) => const HomePage(), // Native Home
+ '/profile': (context) => const ProfilePage(), // Added Profile Route
+ '/webview_fallback': (context) =>
+ const WebViewPage(), // Keep for fallback
+ '/bluetooth': (context) => const BluetoothPage(),
+ '/wifi-config': (context) => const WifiConfigPage(),
+ '/device-control': (context) => const DeviceControlPage(),
},
// Handle unknown routes
onUnknownRoute: (settings) {
diff --git a/airhub_app/lib/pages/bluetooth_page.dart b/airhub_app/lib/pages/bluetooth_page.dart
new file mode 100644
index 0000000..cb409ee
--- /dev/null
+++ b/airhub_app/lib/pages/bluetooth_page.dart
@@ -0,0 +1,689 @@
+import 'dart:async';
+import 'dart:math';
+import 'package:flutter/material.dart';
+
+import 'package:permission_handler/permission_handler.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import '../theme/app_colors.dart';
+
+/// 设备类型
+enum DeviceType { plush, badgeAi, badge }
+
+/// 模拟设备数据模型
+class MockDevice {
+ final String sn;
+ final String name;
+ final DeviceType type;
+ final bool hasAI;
+
+ const MockDevice({
+ required this.sn,
+ required this.name,
+ required this.type,
+ required this.hasAI,
+ });
+
+ String get iconPath {
+ switch (type) {
+ case DeviceType.plush:
+ return 'assets/www/icons/pixel-capybara.svg';
+ case DeviceType.badgeAi:
+ return 'assets/www/icons/pixel-badge-ai.svg';
+ case DeviceType.badge:
+ return 'assets/www/icons/pixel-badge-basic.svg';
+ }
+ }
+
+ String get typeLabel {
+ switch (type) {
+ case DeviceType.plush:
+ return '毛绒机芯';
+ case DeviceType.badgeAi:
+ return 'AI电子吧唧';
+ case DeviceType.badge:
+ return '普通电子吧唧';
+ }
+ }
+}
+
+/// 蓝牙搜索页面
+class BluetoothPage extends StatefulWidget {
+ const BluetoothPage({super.key});
+
+ @override
+ State createState() => _BluetoothPageState();
+}
+
+class _BluetoothPageState extends State
+ with TickerProviderStateMixin {
+ // 状态
+ bool _isSearching = true;
+ List _devices = [];
+ int _currentIndex = 0;
+ bool _isAnimating = false;
+
+ // 动画控制器
+ late AnimationController _searchAnimController;
+ late AnimationController _cardAnimController;
+ late Animation _cardAnimation;
+
+ // 模拟设备数据
+ static const List _mockDevices = [
+ MockDevice(
+ sn: 'PLUSH_01',
+ name: '卡皮巴拉-001',
+ type: DeviceType.plush,
+ hasAI: true,
+ ),
+ MockDevice(
+ sn: 'BADGE_01',
+ name: 'AI电子吧唧-001',
+ type: DeviceType.badgeAi,
+ hasAI: true,
+ ),
+ MockDevice(
+ sn: 'BADGE_02',
+ name: '电子吧唧-001',
+ type: DeviceType.badge,
+ hasAI: false,
+ ),
+ MockDevice(
+ sn: 'PLUSH_02',
+ name: '卡皮巴拉-002',
+ type: DeviceType.plush,
+ hasAI: true,
+ ),
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+
+ // 搜索动画 (神秘盒子浮动)
+ _searchAnimController = AnimationController(
+ duration: const Duration(seconds: 2),
+ vsync: this,
+ )..repeat(reverse: true);
+
+ // 卡片切换动画
+ _cardAnimController = AnimationController(
+ duration: const Duration(milliseconds: 500),
+ vsync: this,
+ );
+ _cardAnimation = Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: _cardAnimController, curve: Curves.easeOutCubic),
+ );
+
+ // 模拟搜索延迟
+ _startSearch();
+ }
+
+ @override
+ void dispose() {
+ _searchAnimController.dispose();
+ _cardAnimController.dispose();
+ super.dispose();
+ }
+
+ /// 开始搜索 (模拟)
+ Future _startSearch() async {
+ // 请求蓝牙权限
+ await _requestPermissions();
+
+ // 模拟 2 秒搜索延迟
+ await Future.delayed(const Duration(seconds: 2));
+
+ if (mounted) {
+ // 随机选择 1-4 个设备
+ final count = Random().nextInt(4) + 1;
+ setState(() {
+ _devices = _mockDevices.take(count).toList();
+ _isSearching = false;
+ });
+ }
+ }
+
+ /// 请求蓝牙权限
+ Future _requestPermissions() async {
+ // 检查蓝牙状态
+ await Permission.bluetooth.request();
+ await Permission.bluetoothScan.request();
+ await Permission.bluetoothConnect.request();
+ await Permission.location.request();
+ }
+
+ /// 切换到下一个设备
+ void _swipeUp() {
+ if (_isAnimating || _devices.length <= 1) return;
+ _animateToIndex((_currentIndex + 1) % _devices.length, isUp: true);
+ }
+
+ /// 切换到上一个设备
+ void _swipeDown() {
+ if (_isAnimating || _devices.length <= 1) return;
+ _animateToIndex(
+ (_currentIndex - 1 + _devices.length) % _devices.length,
+ isUp: false,
+ );
+ }
+
+ /// 动画切换到指定索引
+ void _animateToIndex(int newIndex, {required bool isUp}) {
+ _isAnimating = true;
+ _cardAnimController.forward(from: 0).then((_) {
+ if (mounted) {
+ setState(() {
+ _currentIndex = newIndex;
+ _isAnimating = false;
+ });
+ }
+ });
+ }
+
+ /// 连接设备
+ void _handleConnect() {
+ if (_devices.isEmpty) return;
+
+ final device = _devices[_currentIndex];
+ // TODO: 保存设备信息到本地存储
+
+ if (device.type == DeviceType.badge) {
+ // 普通吧唧 -> 设备控制页
+ Navigator.of(context).pushReplacementNamed('/device-control');
+ } else {
+ // 其他 -> WiFi 配网页
+ Navigator.of(context).pushReplacementNamed('/wifi-config');
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: Stack(
+ children: [
+ // 渐变背景
+ _buildGradientBackground(),
+ // 内容
+ SafeArea(
+ child: Column(
+ children: [
+ // Header
+ _buildHeader(),
+ // 设备数量提示
+ _buildCountLabel(),
+ // 主内容区域
+ Expanded(
+ child: _isSearching
+ ? _buildSearchingState()
+ : _buildDeviceCards(),
+ ),
+ // Footer
+ _buildFooter(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ /// 渐变背景
+ Widget _buildGradientBackground() {
+ final size = MediaQuery.of(context).size;
+ return Positioned.fill(
+ child: Stack(
+ children: [
+ // Layer 1 - Pink
+ Positioned(
+ bottom: -size.width * 0.5,
+ left: -size.width * 0.5,
+ width: size.width * 2,
+ height: size.width * 2,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: RadialGradient(
+ colors: [
+ const Color(0xFFFFC8DC).withOpacity(0.6),
+ Colors.transparent,
+ ],
+ radius: 0.5,
+ ),
+ ),
+ ),
+ ),
+ // Layer 2 - Cyan
+ Positioned(
+ top: -size.width * 0.5,
+ right: -size.width * 0.5,
+ width: size.width * 2,
+ height: size.width * 2,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: RadialGradient(
+ colors: [
+ const Color(0xFFB4F0F0).withOpacity(0.5),
+ Colors.transparent,
+ ],
+ radius: 0.5,
+ ),
+ ),
+ ),
+ ),
+ // Layer 3 - Lavender
+ Positioned(
+ top: size.height * 0.2,
+ left: size.width * 0.1,
+ width: size.width * 1.2,
+ height: size.width * 1.2,
+ child: Container(
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: RadialGradient(
+ colors: [
+ const Color(0xFFE6D2FA).withOpacity(0.45),
+ Colors.transparent,
+ ],
+ radius: 0.5,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ /// Header - HTML: padding 16px 20px (vertical horizontal)
+ Widget _buildHeader() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
+ child: Row(
+ children: [
+ // 返回按钮 - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border
+ GestureDetector(
+ onTap: () => Navigator.of(context).pop(),
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12), // Rounded square, not circle
+ color: Colors.white.withOpacity(0.6),
+ // No border per HTML
+ ),
+ child: const Icon(
+ Icons.arrow_back_ios_new,
+ size: 18,
+ color: Color(0xFF4B5563), // Gray per HTML, not purple
+ ),
+ ),
+ ),
+ // 标题
+ Expanded(
+ child: Text(
+ '搜索设备',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xFF1F2937),
+ ),
+ ),
+ ),
+ // 占位
+ const SizedBox(width: 40),
+ ],
+ ),
+ );
+ }
+
+ /// 设备数量标签
+ Widget _buildCountLabel() {
+ return AnimatedOpacity(
+ duration: const Duration(milliseconds: 300),
+ opacity: _isSearching ? 0 : 1,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 20),
+ child: _devices.isEmpty
+ ? const SizedBox.shrink()
+ : Text.rich(
+ TextSpan(
+ text: '找到 ',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 14,
+ color: const Color(0xFF9CA3AF),
+ ),
+ children: [
+ TextSpan(
+ text: '${_devices.length}',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xFF8B5CF6),
+ ),
+ ),
+ TextSpan(
+ text: _devices.length > 1 ? ' 个设备 · 滑动切换' : ' 个设备',
+ ),
+ ],
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ }
+
+ /// 搜索中状态
+ Widget _buildSearchingState() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // 神秘盒子动画
+ AnimatedBuilder(
+ animation: _searchAnimController,
+ builder: (context, child) {
+ return Transform.translate(
+ offset: Offset(0, -15 * _searchAnimController.value),
+ child: child,
+ );
+ },
+ // HTML: mystery-box is transparent, icon is 120x120 with amber drop-shadow
+ child: SvgPicture.asset(
+ 'assets/www/icons/pixel-mystery-box.svg',
+ width: 120,
+ height: 120,
+ placeholderBuilder: (_) => Text(
+ '?',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 48,
+ fontWeight: FontWeight.w700,
+ color: const Color(0xFFF59E0B), // Amber color per HTML
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ // 搜索状态文字
+ Text(
+ '正在搜索附近设备',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 16,
+ color: const Color(0xFF4B5563),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ /// 设备卡片区域
+ Widget _buildDeviceCards() {
+ if (_devices.isEmpty) {
+ return Center(
+ child: Text(
+ '未找到设备',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 16,
+ color: const Color(0xFF9CA3AF),
+ ),
+ ),
+ );
+ }
+
+ return Stack(
+ children: [
+ // 卡片容器 (支持滑动)
+ GestureDetector(
+ onVerticalDragEnd: (details) {
+ if (details.primaryVelocity == null) return;
+ if (details.primaryVelocity! < -50) {
+ _swipeUp();
+ } else if (details.primaryVelocity! > 50) {
+ _swipeDown();
+ }
+ },
+ child: Container(
+ color: Colors.transparent,
+ child: Center(child: _buildDeviceCard(_devices[_currentIndex])),
+ ),
+ ),
+ // 右侧指示器
+ if (_devices.length > 1)
+ Positioned(
+ right: 20,
+ top: 0,
+ bottom: 0,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: List.generate(
+ _devices.length,
+ (index) => _buildDot(index == _currentIndex),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// 单个设备卡片
+ Widget _buildDeviceCard(MockDevice device) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 图标 + AI 徽章
+ Stack(
+ clipBehavior: Clip.none,
+ children: [
+ // 设备图标 - HTML: no background wrapper, icon is 120x120
+ SizedBox(
+ width: 120,
+ height: 120,
+ child: _buildDeviceIcon(device),
+ ),
+ // AI 徽章
+ if (device.hasAI)
+ Positioned(
+ top: -4,
+ right: -4,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFF8B5CF6), Color(0xFF6366F1)],
+ ),
+ borderRadius: BorderRadius.circular(8),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF8B5CF6).withOpacity(0.4),
+ offset: const Offset(0, 4),
+ blurRadius: 10,
+ ),
+ ],
+ ),
+ child: Text(
+ 'AI',
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 11,
+ fontWeight: FontWeight.w700,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
+ // 设备名称
+ Text(
+ device.name,
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 24,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xFF1F2937),
+ ),
+ ),
+ const SizedBox(height: 4),
+ // 设备类型
+ Text(
+ device.typeLabel,
+ style: TextStyle(fontFamily: 'Inter',
+ fontSize: 15,
+ color: const Color(0xFF6B7280),
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// 设备图标 - HTML: 120x120 per CSS .card-icon-img
+ Widget _buildDeviceIcon(MockDevice device) {
+ return SvgPicture.asset(
+ device.iconPath,
+ width: 120,
+ height: 120,
+ fit: BoxFit.contain,
+ placeholderBuilder: (_) {
+ IconData icon;
+ switch (device.type) {
+ case DeviceType.plush:
+ icon = Icons.pets;
+ case DeviceType.badgeAi:
+ icon = Icons.smart_toy;
+ case DeviceType.badge:
+ icon = Icons.badge;
+ }
+ return Icon(icon, size: 80, color: const Color(0xFF8B5CF6));
+ },
+ );
+ }
+
+ /// 指示器圆点
+ Widget _buildDot(bool isActive) {
+ return AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeOutCubic,
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ width: 6,
+ height: isActive ? 18 : 6,
+ decoration: BoxDecoration(
+ color: isActive
+ ? const Color(0xFF8B5CF6)
+ : const Color(0xFF8B5CF6).withOpacity(0.2),
+ borderRadius: BorderRadius.circular(isActive ? 3 : 3),
+ ),
+ );
+ }
+
+ /// Footer - HTML: padding 20px 20px 60px, gap 16px, centered buttons
+ Widget _buildFooter() {
+ return Container(
+ padding: EdgeInsets.fromLTRB(
+ 20, // HTML: 20px sides
+ 20, // HTML: 20px top
+ 20,
+ MediaQuery.of(context).padding.bottom + 60, // HTML: safe-area + 60px
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // 取消按钮 - HTML: frosted glass with border
+ GestureDetector(
+ onTap: () => Navigator.of(context).pop(),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(25),
+ 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: Text(
+ _isSearching ? '取消搜索' : '取消',
+ style: const TextStyle(
+ fontFamily: 'Inter',
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF6B7280),
+ ),
+ ),
+ ),
+ ),
+ ),
+ // 连接按钮 (搜索完成后显示)
+ if (!_isSearching && _devices.isNotEmpty) ...[
+ const SizedBox(width: 16), // HTML: gap 16px
+ GestureDetector(
+ onTap: _handleConnect,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 16),
+ decoration: BoxDecoration(
+ gradient: AppColors.btnPrimaryGradient,
+ borderRadius: BorderRadius.circular(29), // HTML: 29px
+ // HTML: 5-layer glow effect
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF22D3EE).withOpacity(0.35),
+ offset: Offset.zero,
+ blurRadius: 15,
+ ),
+ BoxShadow(
+ color: const Color(0xFF6366F1).withOpacity(0.25),
+ offset: Offset.zero,
+ blurRadius: 30,
+ ),
+ BoxShadow(
+ color: const Color(0xFF6366F1).withOpacity(0.4),
+ offset: const Offset(0, 6),
+ blurRadius: 20,
+ ),
+ ],
+ ),
+ child: Stack(
+ children: [
+ // Shine overlay
+ Positioned.fill(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(29),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.white.withOpacity(0.15),
+ Colors.transparent,
+ ],
+ stops: const [0.0, 0.5],
+ ),
+ ),
+ ),
+ ),
+ ),
+ Text(
+ '连接设备',
+ style: const TextStyle(
+ fontFamily: 'Inter',
+ fontSize: 17, // HTML: 17px
+ fontWeight: FontWeight.w600,
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
diff --git a/airhub_app/lib/pages/device_control_page.dart b/airhub_app/lib/pages/device_control_page.dart
new file mode 100644
index 0000000..af6e6f1
--- /dev/null
+++ b/airhub_app/lib/pages/device_control_page.dart
@@ -0,0 +1,1081 @@
+import 'dart:math';
+import 'dart:ui';
+import 'package:flutter/material.dart';
+// import 'package:google_fonts/google_fonts.dart'; (Removed)
+import 'package:flutter_svg/flutter_svg.dart';
+import 'story_detail_page.dart';
+import 'product_selection_page.dart';
+import 'settings_page.dart';
+import '../widgets/glass_dialog.dart';
+import '../widgets/story_generator_modal.dart';
+import 'story_loading_page.dart';
+import 'profile/profile_page.dart';
+import '../theme/design_tokens.dart';
+import '../widgets/dashed_rect.dart';
+
+class DeviceControlPage extends StatefulWidget {
+ const DeviceControlPage({super.key});
+
+ @override
+ State createState() => _DeviceControlPageState();
+}
+
+class _DeviceControlPageState extends State
+ with SingleTickerProviderStateMixin {
+ int _currentIndex = 0; // 0: Home, 1: Story, 2: Music, 3: User
+
+ // Animation for mascot
+ late AnimationController _mascotAnimController;
+
+ // PageController for bookshelf scroll tracking
+ late PageController _bookshelfController;
+ double _bookshelfScrollOffset = 0.0;
+
+ // Animation for new book
+ int? _newBookIndex;
+
+ final List