登录页面dart 重写
This commit is contained in:
parent
05fd2ec61a
commit
54f13da9e3
@ -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());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
832
airhub_app/lib/pages/login_page.dart
Normal file
832
airhub_app/lib/pages/login_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
airhub_app/lib/pages/webview_page.dart
Normal file
82
airhub_app/lib/pages/webview_page.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
airhub_app/lib/theme/app_colors.dart
Normal file
62
airhub_app/lib/theme/app_colors.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
71
airhub_app/lib/theme/app_theme.dart
Normal file
71
airhub_app/lib/theme/app_theme.dart
Normal 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);
|
||||
}
|
||||
90
airhub_app/lib/widgets/gradient_button.dart
Normal file
90
airhub_app/lib/widgets/gradient_button.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user