- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化 - 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失) - 书架页: bookPop弹出+粒子效果(三段式动画完整链路) - 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局 - 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化 - 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌 - 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面 - 阶段总结文档更新 Co-authored-by: Cursor <cursoragent@cursor.com>
258 lines
7.5 KiB
Dart
258 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../theme/app_colors.dart';
|
|
import '../widgets/animated_gradient_background.dart';
|
|
|
|
class HomePage extends StatefulWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
State<HomePage> createState() => _HomePageState();
|
|
}
|
|
|
|
class _HomePageState extends State<HomePage>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _mascotController;
|
|
late Animation<double> _mascotAnimation;
|
|
late AnimationController _shineController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Mascot floating animation
|
|
_mascotController = AnimationController(
|
|
duration: const Duration(seconds: 4),
|
|
vsync: this,
|
|
)..repeat(reverse: true);
|
|
|
|
_mascotAnimation = Tween<double>(begin: -10, end: 10).animate(
|
|
CurvedAnimation(parent: _mascotController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
// Shine sweep animation for connect button
|
|
_shineController = AnimationController(
|
|
duration: const Duration(seconds: 4),
|
|
vsync: this,
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_mascotController.dispose();
|
|
_shineController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleConnect() {
|
|
context.go('/bluetooth');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: Stack(
|
|
children: [
|
|
// Gradient Background
|
|
_buildGradientBackground(),
|
|
|
|
SafeArea(
|
|
child: Column(
|
|
children: [
|
|
// Header (Logo)
|
|
_buildHeader(),
|
|
|
|
// Main Content (Mascot)
|
|
Expanded(child: _buildBody()),
|
|
|
|
// Footer (Button)
|
|
_buildFooter(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGradientBackground() {
|
|
return const AnimatedGradientBackground();
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.only(top: 32),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
'Airhub',
|
|
// Use Press Start 2P pixel font per HTML CSS
|
|
style: GoogleFonts.pressStart2p(
|
|
fontSize: 28,
|
|
color: const Color(0xFF6366F1), // 靛蓝
|
|
letterSpacing: 2,
|
|
shadows: const [
|
|
Shadow(
|
|
color: Color(0x30A78BFA),
|
|
offset: Offset(1, 1),
|
|
blurRadius: 0,
|
|
),
|
|
Shadow(
|
|
color: Color(0x1AA78BFA),
|
|
offset: Offset(2, 2),
|
|
blurRadius: 0,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
return Center(
|
|
child: AnimatedBuilder(
|
|
animation: _mascotAnimation,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, _mascotAnimation.value),
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
// Glow effect behind mascot
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.15),
|
|
blurRadius: 40,
|
|
spreadRadius: 10,
|
|
),
|
|
],
|
|
),
|
|
child: Transform.translate(
|
|
offset: const Offset(16, 0), // HTML: translateX(5%) ≈ 16px on 320
|
|
child: Image.asset(
|
|
'assets/www/mascot_transparent.png',
|
|
width: 320, // HTML: min(320px, 75vw)
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) =>
|
|
const Icon(Icons.adb, size: 200, color: Colors.grey),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFooter() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 56),
|
|
child: Container(
|
|
height: 58, // HTML: height: 58px
|
|
constraints: const BoxConstraints(maxWidth: 300), // HTML: width: min(300px, 82vw)
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(29), // HTML: border-radius: 29px
|
|
gradient: AppColors.btnPrimaryGradient,
|
|
// 5-layer box-shadow per HTML CSS --btn-primary-glow
|
|
boxShadow: [
|
|
// 0 0 15px rgba(34, 211, 238, 0.35) - cyan outer glow
|
|
BoxShadow(
|
|
color: const Color(0xFF22D3EE).withOpacity(0.35),
|
|
offset: Offset.zero,
|
|
blurRadius: 15,
|
|
),
|
|
// 0 0 30px rgba(99, 102, 241, 0.25) - indigo wider glow
|
|
BoxShadow(
|
|
color: const Color(0xFF6366F1).withOpacity(0.25),
|
|
offset: Offset.zero,
|
|
blurRadius: 30,
|
|
),
|
|
// 0 6px 20px rgba(99, 102, 241, 0.4) - bottom shadow
|
|
BoxShadow(
|
|
color: const Color(0xFF6366F1).withOpacity(0.4),
|
|
offset: const Offset(0, 6),
|
|
blurRadius: 20,
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Shine overlay (top half gradient)
|
|
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],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Sweep shine animation (btn-shine from HTML)
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(29),
|
|
child: AnimatedBuilder(
|
|
animation: _shineController,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: Offset(
|
|
(_shineController.value * 2 - 1) * 200,
|
|
0,
|
|
),
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.white.withOpacity(0.10),
|
|
Colors.transparent,
|
|
],
|
|
stops: const [0.0, 0.5, 1.0],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Button content
|
|
Material(
|
|
color: Colors.transparent,
|
|
elevation: 0,
|
|
child: InkWell(
|
|
onTap: _handleConnect,
|
|
borderRadius: BorderRadius.circular(29),
|
|
child: Center(
|
|
child: Text(
|
|
'立即连接',
|
|
style: GoogleFonts.dmSans(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|