Major UI/UX and architecture improvements
- Implemented new navigation system with NavigationProvider and route management - Added adaptive theme system with ThemeProvider for better theme handling - Introduced glassmorphism design elements (app bars, scaffolds, cards) - Added advanced animations (spring animations, page transitions, staggered lists) - Implemented performance optimizations (memory manager, lazy loading) - Refactored Analysis screen into modular components - Added floating navigation bar with haptic feedback - Improved subscription cards with swipe actions - Enhanced skeleton loading with better animations - Added cached network image support - Improved overall app architecture and code organization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,6 @@ import 'dart:math' as math;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/category_provider.dart';
|
||||
import '../models/category_model.dart';
|
||||
import '../services/sms_service.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import '../services/exchange_rate_service.dart';
|
||||
@@ -495,7 +494,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, true); // 성공 여부 반환
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
@@ -536,11 +535,11 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(appBarOpacity),
|
||||
color: Colors.white.withValues(alpha: appBarOpacity),
|
||||
boxShadow: appBarOpacity > 0.6
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1 * appBarOpacity),
|
||||
color: Colors.black.withValues(alpha: 0.1 * appBarOpacity),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
@@ -561,7 +560,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
shadows: appBarOpacity > 0.6
|
||||
? [
|
||||
Shadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
offset: const Offset(0, 1),
|
||||
blurRadius: 2,
|
||||
)
|
||||
@@ -626,7 +625,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _gradientColors[0].withOpacity(0.3),
|
||||
color: _gradientColors[0].withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 8),
|
||||
@@ -638,7 +637,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -741,7 +740,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 0
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -786,7 +785,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -821,7 +820,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 1
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -922,7 +921,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color:
|
||||
Colors.grey.withOpacity(0.2),
|
||||
Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -979,7 +978,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: Border.all(
|
||||
color: _currentEditingField == 1
|
||||
? const Color(0xFF3B82F6)
|
||||
: Colors.grey.withOpacity(
|
||||
: Colors.grey.withValues(alpha:
|
||||
0.4), // 포커스 없을 때 더 진한 회색
|
||||
width: _currentEditingField == 1
|
||||
? 2
|
||||
@@ -997,7 +996,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Colors.grey
|
||||
.withOpacity(0.2),
|
||||
.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1248,7 +1247,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 2
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1285,7 +1284,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1336,7 +1335,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 3
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1397,7 +1396,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: Border.all(
|
||||
color: _nextBillingDate == null
|
||||
? Colors.red
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
@@ -1437,7 +1436,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 4
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1476,7 +1475,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1504,7 +1503,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 5
|
||||
? const Color(0xFF3B82F6).withOpacity(0.1)
|
||||
? const Color(0xFF3B82F6).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1538,7 +1537,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color:
|
||||
Colors.grey.withOpacity(0.2),
|
||||
Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
@@ -1598,7 +1597,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1667,7 +1666,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: Border.all(
|
||||
color: _isEventActive
|
||||
? const Color(0xFF3B82F6)
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
width: _isEventActive ? 2 : 1,
|
||||
),
|
||||
),
|
||||
@@ -1761,7 +1760,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -1825,7 +1824,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -1889,7 +1888,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1967,15 +1966,15 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF3B82F6),
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey.withOpacity(0.3),
|
||||
disabledBackgroundColor: Colors.grey.withValues(alpha: 0.3),
|
||||
disabledForegroundColor:
|
||||
Colors.white.withOpacity(0.5),
|
||||
Colors.white.withValues(alpha: 0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: _isSaveHovered ? 8 : 4,
|
||||
shadowColor: const Color(0xFF3B82F6).withOpacity(0.5),
|
||||
shadowColor: const Color(0xFF3B82F6).withValues(alpha: 0.5),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -385,7 +385,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
return LinearGradient(
|
||||
colors: [
|
||||
baseColor,
|
||||
baseColor.withOpacity(0.7),
|
||||
baseColor.withValues(alpha: 0.7),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
@@ -628,11 +628,11 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(appBarOpacity),
|
||||
color: Colors.white.withValues(alpha: appBarOpacity),
|
||||
boxShadow: appBarOpacity > 0.6
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1 * appBarOpacity),
|
||||
color: Colors.black.withValues(alpha: 0.1 * appBarOpacity),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
@@ -653,7 +653,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
shadows: appBarOpacity > 0.6
|
||||
? [
|
||||
Shadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
offset: const Offset(0, 1),
|
||||
blurRadius: 2,
|
||||
)
|
||||
@@ -746,7 +746,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
tag: 'subscription_${widget.subscription.id}',
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shadowColor: baseColor.withOpacity(0.4),
|
||||
shadowColor: baseColor.withValues(alpha: 0.4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
@@ -760,7 +760,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
baseColor.withOpacity(0.8),
|
||||
baseColor.withValues(alpha: 0.8),
|
||||
baseColor,
|
||||
],
|
||||
),
|
||||
@@ -787,7 +787,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black
|
||||
.withOpacity(0.1),
|
||||
.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
@@ -834,7 +834,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
Colors.white.withOpacity(0.8),
|
||||
Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -846,7 +846,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
@@ -863,7 +863,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
Colors.white.withOpacity(0.8),
|
||||
Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -889,7 +889,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
Colors.white.withOpacity(0.8),
|
||||
Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -924,10 +924,10 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDC2626)
|
||||
.withOpacity(0.2),
|
||||
.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1015,7 +1015,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 0
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1053,7 +1053,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1080,7 +1080,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 1
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1181,7 +1181,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color:
|
||||
Colors.grey.withOpacity(0.2),
|
||||
Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1238,7 +1238,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: Border.all(
|
||||
color: _currentEditingField == 1
|
||||
? baseColor
|
||||
: Colors.grey.withOpacity(
|
||||
: Colors.grey.withValues(alpha:
|
||||
0.4), // 포커스 없을 때 더 진한 회색
|
||||
width: _currentEditingField == 1
|
||||
? 2
|
||||
@@ -1256,7 +1256,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Colors.grey
|
||||
.withOpacity(0.2),
|
||||
.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1508,7 +1508,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 2
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1545,7 +1545,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1584,7 +1584,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 3
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1642,7 +1642,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
@@ -1678,7 +1678,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 4
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1716,7 +1716,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1748,7 +1748,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: _currentEditingField == 5
|
||||
? baseColor.withOpacity(0.1)
|
||||
? baseColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -1776,7 +1776,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
@@ -1827,7 +1827,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -1900,7 +1900,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: Border.all(
|
||||
color: _isEventActive
|
||||
? baseColor
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
width: _isEventActive ? 2 : 1,
|
||||
),
|
||||
),
|
||||
@@ -1990,7 +1990,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -2054,7 +2054,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
@@ -2118,7 +2118,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
@@ -2196,7 +2196,7 @@ class _DetailScreenState extends State<DetailScreen>
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: _isSaveHovered ? 8 : 4,
|
||||
shadowColor: baseColor.withOpacity(0.5),
|
||||
shadowColor: baseColor.withValues(alpha: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
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 '../providers/navigation_provider.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import '../models/subscription_model.dart';
|
||||
import 'add_subscription_screen.dart';
|
||||
import '../routes/app_routes.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';
|
||||
import '../widgets/floating_navigation_bar.dart';
|
||||
import '../widgets/glassmorphic_scaffold.dart';
|
||||
import '../widgets/glassmorphic_app_bar.dart';
|
||||
import '../widgets/home_content.dart';
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
const MainScreen({super.key});
|
||||
@@ -40,7 +32,11 @@ class _MainScreenState extends State<MainScreen>
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _waveController;
|
||||
late ScrollController _scrollController;
|
||||
double _scrollOffset = 0;
|
||||
late FloatingNavBarScrollController _navBarScrollController;
|
||||
bool _isNavBarVisible = true;
|
||||
|
||||
// 화면 목록
|
||||
late final List<Widget> _screens;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -67,12 +63,30 @@ class _MainScreenState extends State<MainScreen>
|
||||
waveController: _waveController,
|
||||
);
|
||||
|
||||
_scrollController = ScrollController()
|
||||
..addListener(() {
|
||||
setState(() {
|
||||
_scrollOffset = _scrollController.offset;
|
||||
});
|
||||
});
|
||||
_scrollController = ScrollController();
|
||||
|
||||
_navBarScrollController = FloatingNavBarScrollController(
|
||||
scrollController: _scrollController,
|
||||
onHide: () => setState(() => _isNavBarVisible = false),
|
||||
onShow: () => setState(() => _isNavBarVisible = true),
|
||||
);
|
||||
|
||||
// 화면 목록 초기화
|
||||
_screens = [
|
||||
HomeContent(
|
||||
fadeController: _fadeController,
|
||||
rotateController: _rotateController,
|
||||
slideController: _slideController,
|
||||
pulseController: _pulseController,
|
||||
waveController: _waveController,
|
||||
scrollController: _scrollController,
|
||||
onAddPressed: () => _navigateToAddSubscription(context),
|
||||
),
|
||||
const AnalysisScreen(),
|
||||
Container(), // 추가 버튼은 별도 처리
|
||||
const SmsScanScreen(),
|
||||
const SettingsScreen(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,6 +104,7 @@ class _MainScreenState extends State<MainScreen>
|
||||
);
|
||||
|
||||
_scrollController.dispose();
|
||||
_navBarScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -136,307 +151,109 @@ class _MainScreenState extends State<MainScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToSmsScan(BuildContext context) async {
|
||||
final added = await Navigator.push<bool>(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
const SmsScanScreen(),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
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<Offset>(
|
||||
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<double>(begin: 0.8, end: 1.0).animate(animation),
|
||||
child: child,
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
AppRoutes.addSubscription,
|
||||
).then((result) {
|
||||
_resetAnimations();
|
||||
|
||||
// 구독이 성공적으로 추가된 경우
|
||||
if (result == true) {
|
||||
// 상단에 스낵바 표시
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
},
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'구독이 추가되었습니다',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981), // 초록색
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 16, // 상단 여백
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.of(context).size.height - 120, // 상단에 위치하도록 bottom 마진 설정
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
),
|
||||
)
|
||||
.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<Offset>(
|
||||
begin: const Offset(1, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
void _handleNavigation(int index, BuildContext context) {
|
||||
final navigationProvider = context.read<NavigationProvider>();
|
||||
|
||||
// 이미 같은 인덱스면 무시
|
||||
if (navigationProvider.currentIndex == index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 추가 버튼은 별도 처리
|
||||
if (index == 2) {
|
||||
_navigateToAddSubscription(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 인덱스 업데이트
|
||||
navigationProvider.updateCurrentIndex(index);
|
||||
}
|
||||
|
||||
@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<SubscriptionProvider>()),
|
||||
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<double>(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<double>(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>(Color(0xFF3B82F6)),
|
||||
),
|
||||
);
|
||||
final navigationProvider = context.watch<NavigationProvider>();
|
||||
final hour = DateTime.now().hour;
|
||||
List<Color> backgroundGradient;
|
||||
|
||||
// 시간대별 배경 그라디언트 설정
|
||||
if (hour >= 6 && hour < 10) {
|
||||
backgroundGradient = AppColors.morningGradient;
|
||||
} else if (hour >= 10 && hour < 17) {
|
||||
backgroundGradient = AppColors.dayGradient;
|
||||
} else if (hour >= 17 && hour < 20) {
|
||||
backgroundGradient = AppColors.eveningGradient;
|
||||
} else {
|
||||
backgroundGradient = AppColors.nightGradient;
|
||||
}
|
||||
|
||||
if (provider.subscriptions.isEmpty) {
|
||||
return EmptyStateWidget(
|
||||
fadeController: _fadeController,
|
||||
rotateController: _rotateController,
|
||||
slideController: _slideController,
|
||||
onAddPressed: () => _navigateToAddSubscription(context),
|
||||
);
|
||||
// 현재 인덱스가 유효한지 확인
|
||||
int currentIndex = navigationProvider.currentIndex;
|
||||
if (currentIndex == 2) {
|
||||
currentIndex = 0; // 추가 버튼은 홈으로 표시
|
||||
}
|
||||
|
||||
// 카테고리별 구독 구분
|
||||
final categoryProvider =
|
||||
Provider.of<CategoryProvider>(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<Offset>(
|
||||
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<Offset>(
|
||||
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<Offset>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
return GlassmorphicScaffold(
|
||||
body: IndexedStack(
|
||||
index: currentIndex == 3 ? 3 : currentIndex == 4 ? 4 : currentIndex,
|
||||
children: _screens,
|
||||
),
|
||||
backgroundGradient: backgroundGradient,
|
||||
useFloatingNavBar: true,
|
||||
floatingNavBarIndex: navigationProvider.currentIndex,
|
||||
onFloatingNavBarTapped: (index) {
|
||||
_handleNavigation(index, context);
|
||||
},
|
||||
enableParticles: false,
|
||||
enableWaveAnimation: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../providers/app_lock_provider.dart';
|
||||
import '../providers/notification_provider.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/navigation_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
@@ -11,6 +12,13 @@ import 'package:path/path.dart' as path;
|
||||
import '../services/notification_service.dart';
|
||||
import '../screens/sms_scan_screen.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../providers/theme_provider.dart';
|
||||
import '../theme/adaptive_theme.dart';
|
||||
import '../widgets/glassmorphic_scaffold.dart';
|
||||
import '../widgets/glassmorphic_app_bar.dart';
|
||||
import '../widgets/glassmorphism_card.dart';
|
||||
import '../widgets/app_navigator.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -27,13 +35,13 @@ class SettingsScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.2)
|
||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
@@ -130,12 +138,81 @@ class SettingsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('설정'),
|
||||
),
|
||||
body: ListView(
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
children: [
|
||||
// 테마 설정
|
||||
GlassmorphismCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Consumer<ThemeProvider>(
|
||||
builder: (context, themeProvider, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'테마 설정',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 테마 모드 선택
|
||||
ListTile(
|
||||
title: const Text('테마 모드'),
|
||||
subtitle: Text(_getThemeModeText(themeProvider.themeMode)),
|
||||
leading: Icon(
|
||||
_getThemeModeIcon(themeProvider.themeMode),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
trailing: DropdownButton<AppThemeMode>(
|
||||
value: themeProvider.themeMode,
|
||||
underline: Container(),
|
||||
onChanged: (mode) {
|
||||
if (mode != null) {
|
||||
themeProvider.setThemeMode(mode);
|
||||
}
|
||||
},
|
||||
items: AppThemeMode.values.map((mode) =>
|
||||
DropdownMenuItem(
|
||||
value: mode,
|
||||
child: Text(_getThemeModeText(mode)),
|
||||
),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// 접근성 설정
|
||||
SwitchListTile(
|
||||
title: const Text('큰 텍스트'),
|
||||
subtitle: const Text('텍스트 크기를 크게 표시합니다'),
|
||||
secondary: const Icon(Icons.text_fields),
|
||||
value: themeProvider.largeText,
|
||||
onChanged: themeProvider.setLargeText,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('모션 감소'),
|
||||
subtitle: const Text('애니메이션 효과를 줄입니다'),
|
||||
secondary: const Icon(Icons.slow_motion_video),
|
||||
value: themeProvider.reduceMotion,
|
||||
onChanged: themeProvider.setReduceMotion,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('고대비 모드'),
|
||||
subtitle: const Text('더 선명한 색상으로 표시합니다'),
|
||||
secondary: const Icon(Icons.contrast),
|
||||
value: themeProvider.highContrast,
|
||||
onChanged: themeProvider.setHighContrast,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// 앱 잠금 설정 UI 숨김
|
||||
// Card(
|
||||
// margin: const EdgeInsets.all(16),
|
||||
@@ -161,8 +238,9 @@ class SettingsScreen extends StatelessWidget {
|
||||
// ),
|
||||
|
||||
// 알림 설정
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
GlassmorphismCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Consumer<NotificationProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Column(
|
||||
@@ -211,7 +289,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.3),
|
||||
.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -273,7 +351,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.5),
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
@@ -329,7 +407,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.3),
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
),
|
||||
@@ -377,8 +455,9 @@ class SettingsScreen extends StatelessWidget {
|
||||
),
|
||||
|
||||
// 데이터 관리
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
GlassmorphismCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
// 데이터 백업 기능 비활성화
|
||||
@@ -389,108 +468,14 @@ class SettingsScreen extends StatelessWidget {
|
||||
// onTap: () => _backupData(context),
|
||||
// ),
|
||||
// const Divider(),
|
||||
// SMS 스캔 - 시각적으로 강조된 UI
|
||||
InkWell(
|
||||
onTap: () => _navigateToSmsScan(context),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
Theme.of(context).primaryColor.withOpacity(0.2),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0, horizontal: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(left: 8, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.primaryColor
|
||||
.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.sms_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'SMS 스캔으로 구독 자동 찾기',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'추천',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'2회 이상 반복 결제된 구독 서비스를 자동으로 찾아 추가합니다',
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 앱 정보
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
GlassmorphismCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ListTile(
|
||||
title: const Text('앱 정보'),
|
||||
subtitle: const Text('버전 1.0.0'),
|
||||
@@ -554,8 +539,36 @@ class SettingsScreen extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20 + MediaQuery.of(context).padding.bottom, // 하단 여백
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
String _getThemeModeText(AppThemeMode mode) {
|
||||
switch (mode) {
|
||||
case AppThemeMode.light:
|
||||
return '라이트';
|
||||
case AppThemeMode.dark:
|
||||
return '다크';
|
||||
case AppThemeMode.oled:
|
||||
return 'OLED 블랙';
|
||||
case AppThemeMode.system:
|
||||
return '시스템 설정';
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getThemeModeIcon(AppThemeMode mode) {
|
||||
switch (mode) {
|
||||
case AppThemeMode.light:
|
||||
return Icons.light_mode;
|
||||
case AppThemeMode.dark:
|
||||
return Icons.dark_mode;
|
||||
case AppThemeMode.oled:
|
||||
return Icons.phonelink_lock;
|
||||
case AppThemeMode.system:
|
||||
return Icons.settings_brightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sms_scanner.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/navigation_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/subscription.dart';
|
||||
import '../models/subscription_model.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import 'package:intl/intl.dart'; // NumberFormat을 사용하기 위한 import 추가
|
||||
import '../widgets/glassmorphic_scaffold.dart';
|
||||
import '../widgets/glassmorphic_app_bar.dart';
|
||||
import '../widgets/glassmorphism_card.dart';
|
||||
import '../widgets/themed_text.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
class SmsScanScreen extends StatefulWidget {
|
||||
const SmsScanScreen({super.key});
|
||||
@@ -100,8 +106,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
_filterDuplicates(repeatSubscriptions, existingSubscriptions);
|
||||
print('중복 제거 후 구독: ${filteredSubscriptions.length}개');
|
||||
|
||||
if (filteredSubscriptions.isNotEmpty &&
|
||||
filteredSubscriptions[0] != null) {
|
||||
if (filteredSubscriptions.isNotEmpty) {
|
||||
print(
|
||||
'첫 번째 필터링된 구독: ${filteredSubscriptions[0].serviceName}, 반복 횟수: ${filteredSubscriptions[0].repeatCount}');
|
||||
}
|
||||
@@ -163,10 +168,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
|
||||
// 중복되지 않은 구독만 필터링
|
||||
final nonDuplicates = scanned.where((scannedSub) {
|
||||
if (scannedSub == null) {
|
||||
print('_filterDuplicates: null 구독 객체 발견');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 서비스명과 금액이 동일한 기존 구독 찾기
|
||||
final hasDuplicate = existing.any((existingSub) =>
|
||||
@@ -189,10 +190,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
|
||||
for (int i = 0; i < nonDuplicates.length; i++) {
|
||||
final subscription = nonDuplicates[i];
|
||||
if (subscription == null) {
|
||||
print('_filterDuplicates: null 구독 객체 무시');
|
||||
continue;
|
||||
}
|
||||
|
||||
String? websiteUrl = subscription.websiteUrl;
|
||||
|
||||
@@ -252,11 +249,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
}
|
||||
|
||||
final subscription = _scannedSubscriptions[_currentIndex];
|
||||
if (subscription == null) {
|
||||
print('오류: 현재 인덱스의 구독이 null입니다. (index: $_currentIndex)');
|
||||
_moveToNextSubscription();
|
||||
return;
|
||||
}
|
||||
|
||||
final provider = Provider.of<SubscriptionProvider>(context, listen: false);
|
||||
|
||||
@@ -365,9 +357,38 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${subscription.serviceName} 구독이 추가되었습니다.'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${subscription.serviceName} 구독이 추가되었습니다.',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981), // 초록색
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 16, // 상단 여백
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.of(context).size.height - 120, // 상단에 위치하도록 bottom 마진 설정
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -402,21 +423,36 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
_currentIndex++;
|
||||
_websiteUrlController.text = ''; // URL 입력 필드 초기화
|
||||
|
||||
// 모든 구독을 처리했으면 화면 종료
|
||||
// 모든 구독을 처리했으면 홈 화면으로 이동
|
||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||
Navigator.of(context).pop(true);
|
||||
_navigateToHome();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 홈 화면으로 이동
|
||||
void _navigateToHome() {
|
||||
// NavigationProvider를 사용하여 홈 화면으로 이동
|
||||
final navigationProvider = Provider.of<NavigationProvider>(context, listen: false);
|
||||
navigationProvider.updateCurrentIndex(0);
|
||||
|
||||
// 완료 메시지 표시
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('모든 구독이 처리되었습니다.'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 날짜 상태 텍스트 가져오기
|
||||
String _getNextBillingText(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (date.isBefore(now)) {
|
||||
// 주기에 따라 다음 결제일 예측
|
||||
if (_currentIndex >= _scannedSubscriptions.length ||
|
||||
_scannedSubscriptions[_currentIndex] == null) {
|
||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||
return '다음 결제일 확인 필요';
|
||||
}
|
||||
|
||||
@@ -485,17 +521,13 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('SMS 스캔'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _isLoading
|
||||
? _buildLoadingState()
|
||||
: (_scannedSubscriptions.isEmpty
|
||||
? _buildInitialState()
|
||||
: _buildSubscriptionState())),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _isLoading
|
||||
? _buildLoadingState()
|
||||
: (_scannedSubscriptions.isEmpty
|
||||
? _buildInitialState()
|
||||
: _buildSubscriptionState()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -507,9 +539,9 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('SMS 메시지를 스캔 중입니다...'),
|
||||
ThemedText('SMS 메시지를 스캔 중입니다...'),
|
||||
SizedBox(height: 8),
|
||||
Text('구독 서비스를 찾고 있습니다', style: TextStyle(color: Colors.grey)),
|
||||
ThemedText('구독 서비스를 찾고 있습니다', opacity: 0.7),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -524,24 +556,25 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
if (_errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
child: ThemedText(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
color: Colors.red,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'2회 이상 결제된 구독 서비스 찾기',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Text(
|
||||
child: ThemedText(
|
||||
'문자 메시지를 스캔하여 반복적으로 결제된 구독 서비스를 자동으로 찾습니다. 서비스명과 금액을 추출하여 쉽게 구독을 추가할 수 있습니다.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
opacity: 0.7,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -562,26 +595,11 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
Widget _buildSubscriptionState() {
|
||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||
return const Center(
|
||||
child: Text('모든 구독 처리 완료'),
|
||||
child: ThemedText('모든 구독 처리 완료'),
|
||||
);
|
||||
}
|
||||
|
||||
final subscription = _scannedSubscriptions[_currentIndex];
|
||||
if (subscription == null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('오류: 구독 정보를 불러올 수 없습니다.'),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _moveToNextSubscription,
|
||||
child: Text('건너뛰기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 구독 리스트 카드를 표시할 때 URL 필드 자동 설정
|
||||
if (_websiteUrlController.text.isEmpty && subscription.websiteUrl != null) {
|
||||
@@ -594,54 +612,42 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
// 진행 상태 표시
|
||||
LinearProgressIndicator(
|
||||
value: (_currentIndex + 1) / _scannedSubscriptions.length,
|
||||
backgroundColor: Colors.grey.withOpacity(0.2),
|
||||
backgroundColor: Colors.grey.withValues(alpha: 0.2),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
ThemedText(
|
||||
'${_currentIndex + 1}/${_scannedSubscriptions.length}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 구독 정보 카드
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
GlassmorphismCard(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'다음 구독을 찾았습니다',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 서비스명
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'서비스명',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ThemedText(
|
||||
subscription.serviceName,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -652,15 +658,13 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'월 비용',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ThemedText(
|
||||
subscription.currency == 'USD'
|
||||
? NumberFormat.currency(
|
||||
locale: 'en_US',
|
||||
@@ -672,10 +676,8 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
symbol: '₩',
|
||||
decimalDigits: 0,
|
||||
).format(subscription.monthlyCost),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -684,21 +686,17 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'반복 횟수',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ThemedText(
|
||||
_getRepeatCountText(subscription.repeatCount),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -714,20 +712,16 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'결제 주기',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ThemedText(
|
||||
subscription.billingCycle,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -736,20 +730,16 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
const ThemedText(
|
||||
'결제일',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontWeight: FontWeight.w500,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ThemedText(
|
||||
_getNextBillingText(subscription.nextBillingDate),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -800,7 +790,6 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -809,8 +798,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_scannedSubscriptions.isNotEmpty &&
|
||||
_currentIndex < _scannedSubscriptions.length &&
|
||||
_scannedSubscriptions[_currentIndex] != null) {
|
||||
_currentIndex < _scannedSubscriptions.length) {
|
||||
final currentSub = _scannedSubscriptions[_currentIndex];
|
||||
if (_websiteUrlController.text.isEmpty && currentSub.websiteUrl != null) {
|
||||
_websiteUrlController.text = currentSub.websiteUrl!;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_lock_provider.dart';
|
||||
import '../providers/navigation_provider.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../widgets/glassmorphism_card.dart';
|
||||
import '../routes/app_routes.dart';
|
||||
import 'app_lock_screen.dart';
|
||||
import 'main_screen.dart';
|
||||
|
||||
@@ -101,18 +105,10 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
|
||||
void navigateToNextScreen() {
|
||||
// 앱 잠금 기능 비활성화: 항상 MainScreen으로 이동
|
||||
Navigator.of(context).pushReplacement(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
const MainScreen(),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 500),
|
||||
),
|
||||
// 모든 이전 라우트를 제거하고 홈으로 이동
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
AppRoutes.main,
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,244 +123,305 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: AppColors.blueGradient,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 배경 그라디언트
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppColors.dayGradient[0],
|
||||
AppColors.dayGradient[1],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 배경 파티클
|
||||
..._particles.map((particle) {
|
||||
return AnimatedPositioned(
|
||||
duration: Duration(milliseconds: particle['duration'].toInt()),
|
||||
curve: Curves.easeInOut,
|
||||
left: particle['x'] - 50 + (size.width * 0.1),
|
||||
top: particle['y'] - 50 + (size.height * 0.1),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: particle['opacity']),
|
||||
// 글래스모피즘 오버레이
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
// 배경 파티클
|
||||
..._particles.map((particle) {
|
||||
return AnimatedPositioned(
|
||||
duration:
|
||||
Duration(milliseconds: particle['duration'].toInt()),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: particle['size'],
|
||||
height: particle['size'],
|
||||
decoration: BoxDecoration(
|
||||
color: particle['color'],
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: particle['color'].withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
// 상단 원형 그라데이션
|
||||
Positioned(
|
||||
top: -size.height * 0.2,
|
||||
right: -size.width * 0.2,
|
||||
child: Container(
|
||||
width: size.width * 0.8,
|
||||
height: size.width * 0.8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(0.1),
|
||||
Colors.white.withOpacity(0.0),
|
||||
],
|
||||
stops: const [0.2, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 하단 원형 그라데이션
|
||||
Positioned(
|
||||
bottom: -size.height * 0.1,
|
||||
left: -size.width * 0.3,
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.width * 0.9,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(0.07),
|
||||
Colors.white.withOpacity(0.0),
|
||||
],
|
||||
stops: const [0.4, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 메인 콘텐츠
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 로고 애니메이션
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotateAnimation.value,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, _) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) =>
|
||||
LinearGradient(
|
||||
colors: AppColors.blueGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
).createShader(bounds),
|
||||
child: Icon(
|
||||
Icons.subscriptions_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context)
|
||||
.primaryColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 앱 이름 텍스트
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'SubManager',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
left: particle['x'] - 50 + (size.width * 0.1),
|
||||
top: particle['y'] - 50 + (size.height * 0.1),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: particle['opacity']),
|
||||
duration:
|
||||
Duration(milliseconds: particle['duration'].toInt()),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: particle['size'],
|
||||
height: particle['size'],
|
||||
decoration: BoxDecoration(
|
||||
color: particle['color'],
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: particle['color'].withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 부제목 텍스트
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value * 1.2),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'구독 서비스 관리를 더 쉽게',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white70,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// 로딩 인디케이터
|
||||
FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 카피라이트 텍스트
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: const Text(
|
||||
'© 2023 CClabs. All rights reserved.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white60,
|
||||
letterSpacing: 0.5,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
// 상단 원형 그라데이션
|
||||
Positioned(
|
||||
top: -size.height * 0.2,
|
||||
right: -size.width * 0.2,
|
||||
child: Container(
|
||||
width: size.width * 0.8,
|
||||
height: size.width * 0.8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.1),
|
||||
Colors.white.withValues(alpha: 0.0),
|
||||
],
|
||||
stops: const [0.2, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 하단 원형 그라데이션
|
||||
Positioned(
|
||||
bottom: -size.height * 0.1,
|
||||
left: -size.width * 0.3,
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.width * 0.9,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.07),
|
||||
Colors.white.withValues(alpha: 0.0),
|
||||
],
|
||||
stops: const [0.4, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 메인 콘텐츠
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 로고 애니메이션
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotateAnimation.value,
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
const Duration(milliseconds: 200),
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(30),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white
|
||||
.withValues(alpha: 0.2),
|
||||
Colors.white
|
||||
.withValues(alpha: 0.1),
|
||||
],
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(30),
|
||||
border: Border.all(
|
||||
color: Colors.white
|
||||
.withValues(alpha: 0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black
|
||||
.withValues(alpha: 0.1),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 30,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation:
|
||||
_animationController,
|
||||
builder: (context, _) {
|
||||
return ShaderMask(
|
||||
blendMode:
|
||||
BlendMode.srcIn,
|
||||
shaderCallback:
|
||||
(bounds) =>
|
||||
LinearGradient(
|
||||
colors: AppColors
|
||||
.blueGradient,
|
||||
begin:
|
||||
Alignment.topLeft,
|
||||
end: Alignment
|
||||
.bottomRight,
|
||||
).createShader(bounds),
|
||||
child: Icon(
|
||||
Icons
|
||||
.subscriptions_outlined,
|
||||
size: 64,
|
||||
color:
|
||||
Theme.of(context)
|
||||
.primaryColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
));
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 앱 이름 텍스트
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'SubManager',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 부제목 텍스트
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Transform.translate(
|
||||
offset:
|
||||
Offset(0, _slideAnimation.value * 1.2),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'구독 서비스 관리를 더 쉽게',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white70,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// 로딩 인디케이터
|
||||
FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: BackdropFilter(
|
||||
filter:
|
||||
ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
border: Border.all(
|
||||
color:
|
||||
Colors.white.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 카피라이트 텍스트
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: const Text(
|
||||
'© 2023 CClabs. All rights reserved.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white60,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user