import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'dart:math' as math; import 'package:intl/intl.dart'; import '../providers/subscription_provider.dart'; import '../providers/app_lock_provider.dart'; import '../theme/app_colors.dart'; import '../services/subscription_url_matcher.dart'; import '../models/subscription_model.dart'; import 'add_subscription_screen.dart'; import 'analysis_screen.dart'; import 'app_lock_screen.dart'; import 'settings_screen.dart'; import '../widgets/subscription_card.dart'; import '../widgets/skeleton_loading.dart'; import 'sms_scan_screen.dart'; import '../providers/category_provider.dart'; import '../utils/subscription_category_helper.dart'; import '../utils/animation_controller_helper.dart'; import '../widgets/subscription_list_widget.dart'; import '../widgets/main_summary_card.dart'; import '../widgets/empty_state_widget.dart'; import '../widgets/native_ad_widget.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State createState() => _MainScreenState(); } class _MainScreenState extends State with WidgetsBindingObserver, TickerProviderStateMixin { late AnimationController _fadeController; late AnimationController _scaleController; late AnimationController _rotateController; late AnimationController _slideController; late AnimationController _pulseController; late AnimationController _waveController; late ScrollController _scrollController; double _scrollOffset = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _checkAppLock(); // 애니메이션 컨트롤러 초기화 _fadeController = AnimationController(vsync: this); _scaleController = AnimationController(vsync: this); _rotateController = AnimationController(vsync: this); _slideController = AnimationController(vsync: this); _pulseController = AnimationController(vsync: this); _waveController = AnimationController(vsync: this); // 헬퍼 클래스를 사용해 애니메이션 컨트롤러 초기화 AnimationControllerHelper.initControllers( vsync: this, fadeController: _fadeController, scaleController: _scaleController, rotateController: _rotateController, slideController: _slideController, pulseController: _pulseController, waveController: _waveController, ); _scrollController = ScrollController() ..addListener(() { setState(() { _scrollOffset = _scrollController.offset; }); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); // 헬퍼 클래스를 사용해 애니메이션 컨트롤러 해제 AnimationControllerHelper.disposeControllers( fadeController: _fadeController, scaleController: _scaleController, rotateController: _rotateController, slideController: _slideController, pulseController: _pulseController, waveController: _waveController, ); _scrollController.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { // 앱이 백그라운드로 갈 때 final appLockProvider = context.read(); if (appLockProvider.isBiometricEnabled) { appLockProvider.lock(); } } else if (state == AppLifecycleState.resumed) { // 앱이 포그라운드로 돌아올 때 _checkAppLock(); _resetAnimations(); } } void _resetAnimations() { AnimationControllerHelper.resetAnimations( fadeController: _fadeController, scaleController: _scaleController, slideController: _slideController, pulseController: _pulseController, waveController: _waveController, ); } Future _checkAppLock() async { final appLockProvider = context.read(); if (appLockProvider.isLocked) { await Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const AppLockScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: child, ); }, ), ); } } void _navigateToSmsScan(BuildContext context) async { final added = await Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const SmsScanScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween( begin: const Offset(1, 0), end: Offset.zero, ).animate(animation), child: child, ); }, ), ); if (added == true && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('구독이 성공적으로 추가되었습니다')), ); } _resetAnimations(); } void _navigateToAnalysis(BuildContext context) { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const AnalysisScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween( begin: const Offset(1, 0), end: Offset.zero, ).animate(animation), child: child, ); }, ), ); } void _navigateToAddSubscription(BuildContext context) { HapticFeedback.mediumImpact(); Navigator.of(context) .push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const AddSubscriptionScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: ScaleTransition( scale: Tween(begin: 0.8, end: 1.0).animate(animation), child: child, ), ); }, ), ) .then((_) => _resetAnimations()); } void _navigateToSettings(BuildContext context) { Navigator.of(context).push( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const SettingsScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween( begin: const Offset(1, 0), end: Offset.zero, ).animate(animation), child: child, ); }, ), ); } @override Widget build(BuildContext context) { final double appBarOpacity = math.max(0, math.min(1, _scrollOffset / 100)); return Scaffold( backgroundColor: AppColors.backgroundColor, extendBodyBehindAppBar: true, appBar: _buildAppBar(appBarOpacity), body: _buildBody(context, context.watch()), floatingActionButton: _buildFloatingActionButton(context), ); } PreferredSize _buildAppBar(double appBarOpacity) { return PreferredSize( preferredSize: const Size.fromHeight(60), child: Container( decoration: BoxDecoration( color: AppColors.surfaceColor.withOpacity(appBarOpacity), boxShadow: appBarOpacity > 0.6 ? [ BoxShadow( color: Colors.black.withOpacity(0.06 * appBarOpacity), spreadRadius: 0, blurRadius: 12, offset: const Offset(0, 4), ) ] : null, ), child: SafeArea( child: AppBar( title: FadeTransition( opacity: Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut)), child: const Text( 'SubManager', style: TextStyle( fontFamily: 'Montserrat', fontSize: 26, fontWeight: FontWeight.w800, letterSpacing: -0.5, color: Color(0xFF1E293B), ), ), ), elevation: 0, backgroundColor: Colors.transparent, actions: [ IconButton( icon: const FaIcon(FontAwesomeIcons.chartPie, size: 20, color: Color(0xFF64748B)), tooltip: '분석', onPressed: () => _navigateToAnalysis(context), ), IconButton( icon: const FaIcon(FontAwesomeIcons.sms, size: 20, color: Color(0xFF64748B)), tooltip: 'SMS 스캔', onPressed: () => _navigateToSmsScan(context), ), IconButton( icon: const FaIcon(FontAwesomeIcons.gear, size: 20, color: Color(0xFF64748B)), tooltip: '설정', onPressed: () => _navigateToSettings(context), ), ], ), ), ), ); } Widget _buildFloatingActionButton(BuildContext context) { return AnimatedBuilder( animation: _scaleController, builder: (context, child) { return Transform.scale( scale: Tween(begin: 0.95, end: 1.0) .animate(CurvedAnimation( parent: _scaleController, curve: Curves.easeOutBack)) .value, child: FloatingActionButton.extended( onPressed: () => _navigateToAddSubscription(context), icon: const Icon(Icons.add_rounded), label: const Text( '구독 추가', style: TextStyle( fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), elevation: 4, ), ); }, ); } Widget _buildBody(BuildContext context, SubscriptionProvider provider) { if (provider.isLoading) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF3B82F6)), ), ); } if (provider.subscriptions.isEmpty) { return EmptyStateWidget( fadeController: _fadeController, rotateController: _rotateController, slideController: _slideController, onAddPressed: () => _navigateToAddSubscription(context), ); } // 카테고리별 구독 구분 final categoryProvider = Provider.of(context, listen: false); final categorizedSubscriptions = SubscriptionCategoryHelper.categorizeSubscriptions( provider.subscriptions, categoryProvider, ); return RefreshIndicator( onRefresh: () async { await provider.refreshSubscriptions(); _resetAnimations(); }, color: const Color(0xFF3B82F6), child: CustomScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), slivers: [ SliverToBoxAdapter( child: SizedBox(height: MediaQuery.of(context).padding.top + 60), ), SliverToBoxAdapter( child: NativeAdWidget(key: UniqueKey()), ), SliverToBoxAdapter( child: SlideTransition( position: Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.easeOutCubic)), child: MainScreenSummaryCard( provider: provider, fadeController: _fadeController, pulseController: _pulseController, waveController: _waveController, slideController: _slideController, onTap: () => _navigateToAnalysis(context), ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SlideTransition( position: Tween( begin: const Offset(-0.2, 0), end: Offset.zero, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.easeOutCubic)), child: Text( '나의 구독 서비스', style: Theme.of(context).textTheme.titleLarge, ), ), SlideTransition( position: Tween( begin: const Offset(0.2, 0), end: Offset.zero, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.easeOutCubic)), child: Row( children: [ Text( '${provider.subscriptions.length}개', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryColor, ), ), const SizedBox(width: 4), Icon( Icons.arrow_forward_ios, size: 14, color: AppColors.primaryColor, ), ], ), ), ], ), ), ), SubscriptionListWidget( categorizedSubscriptions: categorizedSubscriptions, fadeController: _fadeController, ), SliverToBoxAdapter( child: SizedBox(height: 100), ), ], ), ); } }