diff --git a/airhub_app/lib/main.dart b/airhub_app/lib/main.dart index a3cc90f..8d461c6 100644 --- a/airhub_app/lib/main.dart +++ b/airhub_app/lib/main.dart @@ -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 createState() => _AirhubWebViewState(); -} - -class _AirhubWebViewState extends State { - 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 {}, - ); - } 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()); + }, ); } } diff --git a/airhub_app/lib/pages/login_page.dart b/airhub_app/lib/pages/login_page.dart new file mode 100644 index 0000000..4dcff0a --- /dev/null +++ b/airhub_app/lib/pages/login_page.dart @@ -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 createState() => _LoginPageState(); +} + +class _LoginPageState extends State 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 _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 5), + vsync: this, + )..repeat(reverse: true); + + _animation = Tween( + 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, + ), + ), + ); + } +} diff --git a/airhub_app/lib/pages/webview_page.dart b/airhub_app/lib/pages/webview_page.dart new file mode 100644 index 0000000..1aec658 --- /dev/null +++ b/airhub_app/lib/pages/webview_page.dart @@ -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 createState() => _WebViewPageState(); +} + +class _WebViewPageState extends State { + late final WebViewController _controller; + + @override + void initState() { + super.initState(); + + late final PlatformWebViewControllerCreationParams params; + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams( + allowsInlineMediaPlayback: true, + mediaTypesRequiringUserAction: const {}, + ); + } 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), + ); + } +} diff --git a/airhub_app/lib/theme/app_colors.dart b/airhub_app/lib/theme/app_colors.dart new file mode 100644 index 0000000..5d330ad --- /dev/null +++ b/airhub_app/lib/theme/app_colors.dart @@ -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], + ); +} diff --git a/airhub_app/lib/theme/app_theme.dart b/airhub_app/lib/theme/app_theme.dart new file mode 100644 index 0000000..fb804a3 --- /dev/null +++ b/airhub_app/lib/theme/app_theme.dart @@ -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); +} diff --git a/airhub_app/lib/widgets/gradient_button.dart b/airhub_app/lib/widgets/gradient_button.dart new file mode 100644 index 0000000..ad23079 --- /dev/null +++ b/airhub_app/lib/widgets/gradient_button.dart @@ -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, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/airhub_app/pubspec.lock b/airhub_app/pubspec.lock index 244ed3e..5e85be4 100644 --- a/airhub_app/pubspec.lock +++ b/airhub_app/pubspec.lock @@ -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" diff --git a/airhub_app/pubspec.yaml b/airhub_app/pubspec.yaml index 64a9d52..6880271 100644 --- a/airhub_app/pubspec.yaml +++ b/airhub_app/pubspec.yaml @@ -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: