登录页面dart 重写

This commit is contained in:
zyc 2026-02-05 11:46:42 +08:00
parent 05fd2ec61a
commit 54f13da9e3
8 changed files with 1344 additions and 80 deletions

View File

@ -1,90 +1,32 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Import for Android features.
import 'package:webview_flutter_android/webview_flutter_android.dart';
// Import for iOS features.
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
import 'pages/login_page.dart';
import 'pages/webview_page.dart';
import 'theme/app_theme.dart';
void main() {
runApp(const MaterialApp(home: AirhubWebView()));
runApp(const AirhubApp());
}
class AirhubWebView extends StatefulWidget {
const AirhubWebView({super.key});
@override
State<AirhubWebView> createState() => _AirhubWebViewState();
}
class _AirhubWebViewState extends State<AirhubWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
// #docregion platform_features
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller =
WebViewController.fromPlatformCreationParams(params);
// #enddocregion platform_features
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint('''
Page resource error:
code: ${error.errorCode}
description: ${error.description}
errorType: ${error.errorType}
isForMainFrame: ${error.isForMainFrame}
''');
},
),
)
..loadFlutterAsset('assets/www/login.html');
// #docregion platform_features
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
// #enddocregion platform_features
_controller = controller;
}
class AirhubApp extends StatelessWidget {
const AirhubApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
bottom: false,
child: WebViewWidget(controller: _controller),
),
return MaterialApp(
title: 'Airhub',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
// Initial Route
home: const LoginPage(),
// Named Routes
routes: {
'/login': (context) => const LoginPage(),
'/home': (context) => const WebViewPage(),
},
// Handle unknown routes
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (_) => const WebViewPage());
},
);
}
}

View File

@ -0,0 +1,832 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_colors.dart';
import '../widgets/gradient_button.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
// State
bool _agreed = false;
bool _isLoading = false;
bool _showSmsView = false;
// SMS Login State
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
int _countdown = 0;
Timer? _countdownTimer;
bool _isSmsSubmitting = false;
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
_countdownTimer?.cancel();
super.dispose();
}
// ========== Agreement Dialog ==========
void _showAgreementDialog({required String action}) {
showDialog(
context: context,
barrierColor: Colors.black.withOpacity(0.5),
builder: (context) => _buildAgreementModal(action),
);
}
Widget _buildAgreementModal(String action) {
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
width: 320,
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Title
Text(
'服务协议',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: const Color(0xFF1F2937),
),
),
const SizedBox(height: 12),
// Content
Text.rich(
TextSpan(
text: '请先阅读并同意',
style: GoogleFonts.inter(
fontSize: 14,
color: const Color(0xFF6B7280),
height: 1.6,
),
children: [
TextSpan(
text: '《用户协议》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
const TextSpan(text: ',以便为您提供更好的服务。'),
],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Buttons
Row(
children: [
// Cancel
Expanded(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(24),
),
alignment: Alignment.center,
child: Text(
'再想想',
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w500,
color: const Color(0xFF6B7280),
),
),
),
),
),
const SizedBox(width: 12),
// Confirm
Expanded(
child: GestureDetector(
onTap: () {
setState(() => _agreed = true);
Navigator.pop(context);
if (action == 'oneclick') {
_doOneClickLogin();
} else if (action == 'sms') {
setState(() => _showSmsView = true);
}
},
child: Container(
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
),
borderRadius: BorderRadius.circular(24),
),
alignment: Alignment.center,
child: Text(
'同意并继续',
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
),
],
),
],
),
),
);
}
// ========== One-Click Login ==========
void _handleOneClickLogin() {
if (!_agreed) {
_showAgreementDialog(action: 'oneclick');
return;
}
_doOneClickLogin();
}
void _doOneClickLogin() {
setState(() => _isLoading = true);
_showToast('正在获取本机号码...');
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) {
_showToast('登录成功');
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() => _isLoading = false);
Navigator.of(context).pushReplacementNamed('/home');
}
});
}
});
}
// ========== SMS Login ==========
void _handleSmsLinkTap() {
if (!_agreed) {
_showAgreementDialog(action: 'sms');
return;
}
setState(() => _showSmsView = true);
}
bool _isValidPhone(String phone) {
return RegExp(r'^1[3-9]\d{9}$').hasMatch(phone);
}
bool get _canSubmitSms {
return _isValidPhone(_phoneController.text) &&
_codeController.text.length == 6;
}
void _sendCode() {
if (!_isValidPhone(_phoneController.text)) {
_showToast('请输入正确的手机号');
return;
}
setState(() => _countdown = 60);
_showToast('验证码已发送');
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown <= 1) {
timer.cancel();
if (mounted) setState(() => _countdown = 0);
} else {
if (mounted) setState(() => _countdown--);
}
});
}
void _submitSmsLogin() {
if (!_canSubmitSms) return;
setState(() => _isSmsSubmitting = true);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) {
_showToast('登录成功');
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() => _isSmsSubmitting = false);
Navigator.of(context).pushReplacementNamed('/home');
}
});
}
});
}
// ========== Toast ==========
void _showToast(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
);
}
// ========== Build ==========
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: true,
body: Stack(
children: [
// Background
_buildGradientBackground(),
// Main Login View
_buildMainLoginView(),
// SMS View (overlay)
if (_showSmsView) _buildSmsView(),
],
),
);
}
// ========== Gradient Background ==========
Widget _buildGradientBackground() {
final size = MediaQuery.of(context).size;
return Positioned.fill(
child: Stack(
children: [
// Layer 1 - Pink (bottom-left)
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 (top-right)
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 (center)
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,
),
),
),
),
],
),
);
}
// ========== Main Login View ==========
Widget _buildMainLoginView() {
final bottomPadding = MediaQuery.of(context).padding.bottom;
final topPadding = MediaQuery.of(context).padding.top;
return SafeArea(
child: Column(
children: [
// Logo - padding-top: calc(env(safe-area-inset-top) + 20px)
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(
'Airhub',
style: GoogleFonts.pressStart2p(
fontSize: 26, // Exact match
color: const Color(0xFF4B2E83),
shadows: [
Shadow(
offset: const Offset(0, 2),
blurRadius: 10,
color: const Color(0xFF8B5CF6).withOpacity(0.3),
),
Shadow(
offset: const Offset(0, 0),
blurRadius: 40,
color: const Color(0xFF8B5CF6).withOpacity(0.15),
),
],
),
),
),
// Mascot - flex: 1, centered
Expanded(child: Center(child: _FloatingMascot())),
// Bottom Form
Padding(
padding: EdgeInsets.fromLTRB(32, 0, 32, bottomPadding + 40),
child: Column(
children: [
// Primary Button - height: 56px, border-radius: 28px
GradientButton(
text: '本机号码一键登录',
onPressed: _handleOneClickLogin,
isLoading: _isLoading,
height: 56,
),
// SMS Link - margin-top: 20px, font-size: 14px
const SizedBox(height: 20),
GestureDetector(
onTap: _handleSmsLinkTap,
child: Text(
'使用验证码登录',
style: GoogleFonts.inter(
fontSize: 14,
color: const Color(0xFF4B2E83).withOpacity(0.7),
),
),
),
// Agreement - margin-top: 28px
const SizedBox(height: 28),
_buildAgreementCheckbox(),
],
),
),
],
),
);
}
// ========== Agreement Checkbox ==========
Widget _buildAgreementCheckbox() {
return GestureDetector(
onTap: () => setState(() => _agreed = !_agreed),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Checkbox - 18x18, border-radius: 5px
Container(
width: 18,
height: 18,
margin: const EdgeInsets.only(top: 1), // Fine-tune alignment
decoration: BoxDecoration(
gradient: _agreed
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
)
: null,
color: _agreed ? null : const Color(0x99FFFFFF),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: _agreed
? Colors.transparent
: const Color(0xFF4B2E83).withOpacity(0.3),
width: 1.5,
),
),
child: _agreed
? const Center(
child: Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
)
: null,
),
const SizedBox(width: 10), // gap: 10px
// Text - font-size: 12px, line-height: 1.6
Flexible(
child: Text.rich(
TextSpan(
text: '我已阅读并同意',
style: GoogleFonts.inter(
fontSize: 12,
color: const Color(0xFF4B2E83).withOpacity(0.6),
height: 1.6,
),
children: [
TextSpan(
text: '《用户协议》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: GoogleFonts.inter(color: const Color(0xFF6366F1)),
),
],
),
),
),
],
),
);
}
// ========== SMS View ==========
Widget _buildSmsView() {
final topPadding = MediaQuery.of(context).padding.top;
final bottomPadding = MediaQuery.of(context).padding.bottom;
return Positioned.fill(
child: Container(
color: Colors.white,
child: Stack(
children: [
// Background
_buildGradientBackground(),
// Content
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header - padding-top: 60px (fixed)
Padding(
padding: const EdgeInsets.fromLTRB(24, 60, 24, 16),
child: _buildBackButton(),
),
// Body
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
32,
60,
32,
bottomPadding + 40,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Heading - font-size: 32px, font-weight: 700
Text(
'欢迎使用 Airhub',
style: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w700,
color: const Color(0xFF4B2E83),
letterSpacing: -0.5,
),
),
const SizedBox(height: 12),
// Subheading - font-size: 15px
Text(
'请输入您的手机号验证登录',
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w400,
color: const Color(0xFF4B2E83).withOpacity(0.6),
),
),
const SizedBox(height: 48),
// Phone Input
_buildPhoneInput(),
const SizedBox(height: 24),
// Code Input
_buildCodeInput(),
const SizedBox(height: 48),
// Submit Button - height: 60px, border-radius: 30px
_buildSmsSubmitButton(),
],
),
),
),
],
),
],
),
),
);
}
Widget _buildBackButton() {
return GestureDetector(
onTap: () => setState(() => _showSmsView = false),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0x66FFFFFF),
border: Border.all(color: const Color(0x99FFFFFF)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
offset: const Offset(0, 4),
blurRadius: 12,
),
],
),
child: const Center(
child: Icon(Icons.arrow_back, size: 22, color: Color(0xFF4B2E83)),
),
),
);
}
Widget _buildPhoneInput() {
return Container(
height: 64,
decoration: BoxDecoration(
color: const Color(0x8CFFFFFF),
border: Border.all(color: const Color(0xCCFFFFFF)),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.03),
offset: const Offset(0, 2),
blurRadius: 10,
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
// Prefix
Container(
padding: const EdgeInsets.only(right: 16),
margin: const EdgeInsets.only(right: 16),
decoration: const BoxDecoration(
border: Border(
right: BorderSide(color: Color(0x1A4B2E83), width: 1),
),
),
child: Text(
'+86',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: const Color(0xFF4B2E83),
),
),
),
// Input
Expanded(
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
maxLength: 11,
style: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w500,
color: const Color(0xFF1F2937),
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请输入手机号',
hintStyle: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w400,
color: const Color(0xFF4B2E83).withOpacity(0.35),
),
counterText: '',
),
cursorColor: const Color(0xFF8B5CF6),
onChanged: (_) => setState(() {}),
),
),
],
),
);
}
Widget _buildCodeInput() {
return Container(
height: 64,
decoration: BoxDecoration(
color: const Color(0x8CFFFFFF),
border: Border.all(color: const Color(0xCCFFFFFF)),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.03),
offset: const Offset(0, 2),
blurRadius: 10,
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
// Input
Expanded(
child: TextField(
controller: _codeController,
keyboardType: TextInputType.number,
maxLength: 6,
style: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w500,
color: const Color(0xFF1F2937),
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: '输入验证码',
hintStyle: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w400,
color: const Color(0xFF4B2E83).withOpacity(0.35),
),
counterText: '',
),
cursorColor: const Color(0xFF8B5CF6),
onChanged: (_) => setState(() {}),
),
),
// Send Button
Container(
padding: const EdgeInsets.only(left: 14),
margin: const EdgeInsets.only(left: 10),
decoration: const BoxDecoration(
border: Border(
left: BorderSide(color: Color(0x1A4B2E83), width: 1),
),
),
child: GestureDetector(
onTap: _countdown > 0 ? null : _sendCode,
child: Text(
_countdown > 0 ? '${_countdown}s' : '获取验证码',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _countdown > 0
? const Color(0xFF9CA3AF)
: const Color(0xFF6366F1),
),
),
),
),
],
),
);
}
Widget _buildSmsSubmitButton() {
final bool enabled = _canSubmitSms && !_isSmsSubmitting;
return GestureDetector(
onTap: enabled ? _submitSmsLogin : null,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: enabled ? 1.0 : 0.6,
child: Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
gradient: AppColors.btnPrimaryGradient,
borderRadius: BorderRadius.circular(30),
boxShadow: enabled
? [
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.3),
offset: const Offset(0, 10),
blurRadius: 30,
),
]
: null,
),
alignment: Alignment.center,
child: _isSmsSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: Text(
'立即登录',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
);
}
}
// ========== Floating Mascot Widget ==========
class _FloatingMascot extends StatefulWidget {
@override
State<_FloatingMascot> createState() => _FloatingMascotState();
}
class _FloatingMascotState extends State<_FloatingMascot>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(
begin: 0,
end: -15,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _animation.value),
child: child,
);
},
child: Container(
width: 220,
height: 220,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.25),
offset: const Offset(0, 20),
blurRadius: 40,
),
],
),
child: Image.asset(
'assets/www/mascot.png',
width: 220,
height: 220,
fit: BoxFit.contain,
),
),
);
}
}

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller =
WebViewController.fromPlatformCreationParams(params);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint('''
Page resource error:
code: ${error.errorCode}
description: ${error.description}
errorType: ${error.errorType}
isForMainFrame: ${error.isForMainFrame}
''');
},
),
)
..loadFlutterAsset(
'assets/www/index.html',
); // CHANGED: Load Home directly
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
_controller = controller;
}
@override
Widget build(BuildContext context) {
// We want the WebView to control the full screen, including status bar usually,
// but SafeArea might be needed if the Web content doesn't handle padding.
// Our CSS handles env(safe-area-inset-top), so we can disable SafeArea here
// or keep top:false.
return Scaffold(
backgroundColor: Colors.white,
body: WebViewWidget(controller: _controller),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
class AppColors {
// Gradient Colors (Backgrounds)
static const Color gradientPink = Color(0xFFFEF0F5);
static const Color gradientLavender = Color(0xFFF5F0FE);
static const Color gradientBlue = Color(0xFFEEF8FC);
static const Color gradientMint = Color(0xFFF0FCFA);
// Primary Colors
static const Color primaryPurple = Color(0xFFA78BFA);
static const Color primaryBlue = Color(0xFF93C5FD);
static const Color primaryPink = Color(0xFFF9A8D4);
static const Color primaryIndigo = Color(0xFF6366F1);
// Additional Primary Colors from Button Gradient
static const Color cyan = Color(0xFF22D3EE); // #22D3EE
static const Color deepPurple = Color(0xFF8B5CF6); // #8B5CF6
// Text Colors
static const Color textPrimary = Color(0xFF1F2937);
static const Color textSecondary = Color(0xFF6B7280);
static const Color textLight = Color(0xFF9CA3AF);
// Backgrounds
static const Color bgBase = Color(0xFFFAFBFC);
static const Color bgCard = Color(
0xB3FFFFFF,
); // rgba(255, 255, 255, 0.7) -> 0.7 * 255 = 178.5 -> B3
// Shadows
static final BoxShadow shadowSoft = BoxShadow(
color: Colors.black.withOpacity(0.04),
offset: const Offset(0, 4),
blurRadius: 24,
);
static final BoxShadow shadowMedium = BoxShadow(
color: Colors.black.withOpacity(0.08),
offset: const Offset(0, 8),
blurRadius: 32,
);
static final BoxShadow shadowButton = BoxShadow(
color: const Color(0xFFA78BFA).withOpacity(0.3),
offset: const Offset(0, 8),
blurRadius: 32,
);
// Gradients
static const LinearGradient btnPrimaryGradient = 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],
);
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: AppColors.bgBase,
primaryColor: AppColors.primaryIndigo,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primaryIndigo,
primary: AppColors.primaryIndigo,
secondary: AppColors.primaryPurple,
surface: AppColors.bgBase,
background: AppColors.bgBase,
),
// We will rely on system fonts for now, to replicate 'Inter' look
// we can adjust weights later.
fontFamilyFallback: const [
'Inter',
'Roboto',
'PingFang SC',
'Helvetica Neue',
],
textTheme: const TextTheme(
// h1 / Large Headings
displayLarge: TextStyle(
color: AppColors.textPrimary,
fontSize: 32,
fontWeight: FontWeight.w700, // Bold
letterSpacing: -0.5,
),
// h2 / Subheadings
displayMedium: TextStyle(
color: AppColors.textPrimary,
fontSize: 24,
fontWeight: FontWeight.w600, // Semi-bold
),
// Body Text
bodyLarge: TextStyle(
color: AppColors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400, // Normal
height: 1.5,
),
bodyMedium: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
),
// Small captions
bodySmall: TextStyle(color: AppColors.textLight, fontSize: 12),
// Button Text
labelLarge: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
}
// Animation Curves from CSS
// --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
static const Curve easeSmooth = Cubic(0.4, 0, 0.2, 1);
// --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
static const Curve easeBounce = Cubic(0.34, 1.56, 0.64, 1);
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
class GradientButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final double width;
final double height;
final bool isLoading;
const GradientButton({
super.key,
required this.text,
this.onPressed,
this.width = double.infinity,
this.height = 56.0,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
// Determine if button is disabled strictly by onPressed being null
// But we still want to show gradient for disabled state? Usually disabled is grey.
// Let's stick to the design where it might just opacity down.
final bool isDisabled = onPressed == null || isLoading;
return Opacity(
opacity: isDisabled ? 0.7 : 1.0,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(height / 2),
gradient: AppColors.btnPrimaryGradient,
boxShadow: [
// 0 4px 20px rgba(99, 102, 241, 0.4)
BoxShadow(
color: const Color(0xFF6366F1).withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 20,
),
// 0 0 40px rgba(139, 92, 246, 0.2)
BoxShadow(
color: const Color(0xFF8B5CF6).withOpacity(0.2),
offset: const Offset(0, 0),
blurRadius: 40,
),
// inset 0 1px 0 rgba(255, 255, 255, 0.2) -> Not directly supported in simple BoxShadow
// can use a top border or inner shadow container trick if needed.
// For now, these outer shadows are sufficient for the "Glow".
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isDisabled ? null : onPressed,
borderRadius: BorderRadius.circular(height / 2),
splashColor: Colors.white.withOpacity(0.2),
highlightColor: Colors.white.withOpacity(0.1),
child: Center(
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: Text(
text,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 17,
fontWeight: FontWeight.w600,
shadows: [
const Shadow(
offset: Offset(0, 1),
blurRadius: 2,
color: Colors.black12,
),
],
),
),
),
),
),
),
);
}
}

View File

@ -33,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
@ -41,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
fake_async:
dependency: transitive
description:
@ -49,6 +65,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.5"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@ -72,6 +104,46 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
@ -104,6 +176,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@ -128,6 +208,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e"
url: "https://pub.dev"
source: hosted
version: "9.2.5"
path:
dependency: transitive
description:
@ -136,6 +232,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
@ -184,6 +328,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
@ -192,6 +344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine:
dependency: transitive
description: flutter
@ -245,6 +405,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.7"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
@ -301,6 +469,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.23.5"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.35.0"
flutter: ">=3.38.4"

View File

@ -32,6 +32,7 @@ dependencies:
sdk: flutter
webview_flutter: ^4.4.2
permission_handler: ^11.0.0 # Good practice for future
google_fonts: ^6.1.0 # For 'Inter' and 'Press Start 2P' fonts
dev_dependencies:
flutter_test: