feat: 글래스모피즘 디자인 시스템 및 색상 가이드 전면 적용
- @doc/color.md 가이드라인에 따른 색상 시스템 전면 개편 - 딥 블루(#2563EB), 스카이 블루(#60A5FA) 메인 컬러로 변경 - 모든 화면과 위젯에 글래스모피즘 효과 일관성 있게 적용 - darkNavy, navyGray 등 새로운 텍스트 색상 체계 도입 - 공통 스낵바 및 다이얼로그 컴포넌트 추가 - Claude AI 프로젝트 컨텍스트 파일(CLAUDE.md) 추가 영향받은 컴포넌트: - 10개 스크린 (main, settings, detail, splash 등) - 30개 이상 위젯 (buttons, cards, forms 등) - 테마 시스템 (AppColors, AppTheme) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
14
CLAUDE.md
Normal file
14
CLAUDE.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Claude 프로젝트 컨텍스트
|
||||||
|
|
||||||
|
## 언어 설정
|
||||||
|
- 모든 답변은 한국어로 제공
|
||||||
|
- 기술 용어는 영어와 한국어 병기 가능
|
||||||
|
|
||||||
|
## 프로젝트 정보
|
||||||
|
- Flutter 기반 구독 관리 앱 (SubManager)
|
||||||
|
- 글래스모피즘 디자인 시스템 적용 중
|
||||||
|
- @doc/color.md의 색상 가이드를 전체 UI에 통일성 있게 적용하는 작업 진행 중
|
||||||
|
|
||||||
|
## 현재 작업
|
||||||
|
- 전체 10개 화면과 50개 이상의 위젯에 통일된 글래스모피즘 스타일 적용
|
||||||
|
- 색상 시스템 업데이트 및 일관성 있는 UI 구현
|
||||||
@@ -6,6 +6,7 @@ import '../providers/subscription_provider.dart';
|
|||||||
import '../providers/category_provider.dart';
|
import '../providers/category_provider.dart';
|
||||||
import '../services/sms_service.dart';
|
import '../services/sms_service.dart';
|
||||||
import '../services/subscription_url_matcher.dart';
|
import '../services/subscription_url_matcher.dart';
|
||||||
|
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||||
|
|
||||||
/// AddSubscriptionScreen의 비즈니스 로직을 관리하는 Controller
|
/// AddSubscriptionScreen의 비즈니스 로직을 관리하는 Controller
|
||||||
class AddSubscriptionController {
|
class AddSubscriptionController {
|
||||||
@@ -232,21 +233,9 @@ class AddSubscriptionController {
|
|||||||
final granted = await SMSService.requestSMSPermission();
|
final granted = await SMSService.requestSMSPermission();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showError(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: const Row(
|
message: 'SMS 권한이 필요합니다.',
|
||||||
children: [
|
|
||||||
Icon(Icons.error_outline, color: Colors.white),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(child: Text('SMS 권한이 필요합니다.')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -256,21 +245,9 @@ class AddSubscriptionController {
|
|||||||
final subscriptions = await SMSService.scanSubscriptions();
|
final subscriptions = await SMSService.scanSubscriptions();
|
||||||
if (subscriptions.isEmpty) {
|
if (subscriptions.isEmpty) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showWarning(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: const Row(
|
message: '구독 관련 SMS를 찾을 수 없습니다.',
|
||||||
children: [
|
|
||||||
Icon(Icons.info_outline, color: Colors.white),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(child: Text('구독 관련 SMS를 찾을 수 없습니다.')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -331,21 +308,9 @@ class AddSubscriptionController {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showError(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Row(
|
message: 'SMS 스캔 중 오류 발생: $e',
|
||||||
children: [
|
|
||||||
const Icon(Icons.error_outline, color: Colors.white),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: Text('SMS 스캔 중 오류 발생: $e')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -399,11 +364,9 @@ class AddSubscriptionController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showError(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Text('저장 중 오류가 발생했습니다: $e'),
|
message: '저장 중 오류가 발생했습니다: $e',
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import '../providers/category_provider.dart';
|
|||||||
import '../services/subscription_url_matcher.dart';
|
import '../services/subscription_url_matcher.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import '../widgets/dialogs/delete_confirmation_dialog.dart';
|
||||||
|
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||||
|
|
||||||
/// DetailScreen의 비즈니스 로직을 관리하는 Controller
|
/// DetailScreen의 비즈니스 로직을 관리하는 Controller
|
||||||
class DetailScreenController {
|
class DetailScreenController {
|
||||||
@@ -313,20 +315,9 @@ class DetailScreenController {
|
|||||||
await provider.updateSubscription(subscription);
|
await provider.updateSubscription(subscription);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showSuccess(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: const Row(
|
message: '구독 정보가 업데이트되었습니다.',
|
||||||
children: [
|
|
||||||
Icon(Icons.check_circle_rounded, color: Colors.white),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Text('구독 정보가 업데이트되었습니다.'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: const Color(0xFF10B981),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환
|
// 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환
|
||||||
@@ -340,26 +331,27 @@ class DetailScreenController {
|
|||||||
/// 구독 삭제
|
/// 구독 삭제
|
||||||
Future<void> deleteSubscription() async {
|
Future<void> deleteSubscription() async {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final provider = Provider.of<SubscriptionProvider>(context, listen: false);
|
// 삭제 확인 다이얼로그 표시
|
||||||
await provider.deleteSubscription(subscription.id);
|
final shouldDelete = await DeleteConfirmationDialog.show(
|
||||||
|
context: context,
|
||||||
|
serviceName: subscription.serviceName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDelete) return;
|
||||||
|
|
||||||
|
// 사용자가 확인한 경우에만 삭제 진행
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
final provider = Provider.of<SubscriptionProvider>(context, listen: false);
|
||||||
SnackBar(
|
await provider.deleteSubscription(subscription.id);
|
||||||
content: const Row(
|
|
||||||
children: [
|
if (context.mounted) {
|
||||||
Icon(Icons.delete_forever_rounded, color: Colors.white),
|
AppSnackBar.showSuccess(
|
||||||
SizedBox(width: 12),
|
context: context,
|
||||||
Text('구독이 삭제되었습니다.'),
|
message: '구독이 삭제되었습니다.',
|
||||||
],
|
icon: Icons.delete_forever_rounded,
|
||||||
),
|
);
|
||||||
behavior: SnackBarBehavior.floating,
|
Navigator.of(context).pop();
|
||||||
backgroundColor: const Color(0xFFDC2626),
|
}
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,21 +363,17 @@ class DetailScreenController {
|
|||||||
final Uri url = Uri.parse(subscription.websiteUrl!);
|
final Uri url = Uri.parse(subscription.websiteUrl!);
|
||||||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showError(
|
||||||
const SnackBar(
|
context: context,
|
||||||
content: Text('웹사이트를 열 수 없습니다.'),
|
message: '웹사이트를 열 수 없습니다.',
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showWarning(
|
||||||
const SnackBar(
|
context: context,
|
||||||
content: Text('웹사이트 정보가 없습니다. 해지는 웹사이트에서 진행해주세요.'),
|
message: '웹사이트 정보가 없습니다. 해지는 웹사이트에서 진행해주세요.',
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../widgets/add_subscription/add_subscription_header.dart';
|
|||||||
import '../widgets/add_subscription/add_subscription_form.dart';
|
import '../widgets/add_subscription/add_subscription_form.dart';
|
||||||
import '../widgets/add_subscription/add_subscription_event_section.dart';
|
import '../widgets/add_subscription/add_subscription_event_section.dart';
|
||||||
import '../widgets/add_subscription/add_subscription_save_button.dart';
|
import '../widgets/add_subscription/add_subscription_save_button.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 새로운 구독을 추가하는 화면
|
/// 새로운 구독을 추가하는 화면
|
||||||
class AddSubscriptionScreen extends StatefulWidget {
|
class AddSubscriptionScreen extends StatefulWidget {
|
||||||
@@ -44,7 +45,7 @@ class _AddSubscriptionScreenState extends State<AddSubscriptionScreen>
|
|||||||
_controller.scrollController.addListener(_onScroll);
|
_controller.scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8FAFC),
|
backgroundColor: AppColors.backgroundColor,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
appBar: AddSubscriptionAppBar(
|
appBar: AddSubscriptionAppBar(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/app_lock_provider.dart';
|
import '../providers/app_lock_provider.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
class AppLockScreen extends StatelessWidget {
|
class AppLockScreen extends StatelessWidget {
|
||||||
const AppLockScreen({super.key});
|
const AppLockScreen({super.key});
|
||||||
@@ -12,25 +13,26 @@ class AppLockScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.lock_outline,
|
Icons.lock_outline,
|
||||||
size: 80,
|
size: 80,
|
||||||
color: Colors.grey,
|
color: AppColors.navyGray,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
Text(
|
||||||
'앱이 잠겨 있습니다',
|
'앱이 잠겨 있습니다',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.darkNavy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'생체 인증으로 잠금을 해제하세요',
|
'생체 인증으로 잠금을 해제하세요',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey,
|
color: AppColors.navyGray,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
@@ -40,8 +42,14 @@ class AppLockScreen extends StatelessWidget {
|
|||||||
final success = await appLock.authenticate();
|
final success = await appLock.authenticate();
|
||||||
if (!success && context.mounted) {
|
if (!success && context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('인증에 실패했습니다. 다시 시도해주세요.'),
|
content: Text(
|
||||||
|
'인증에 실패했습니다. 다시 시도해주세요.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.dangerColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/category_provider.dart';
|
import '../providers/category_provider.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
class CategoryManagementScreen extends StatefulWidget {
|
class CategoryManagementScreen extends StatefulWidget {
|
||||||
const CategoryManagementScreen({super.key});
|
const CategoryManagementScreen({super.key});
|
||||||
@@ -41,8 +42,13 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('카테고리 관리'),
|
title: Text(
|
||||||
backgroundColor: const Color(0xFF1976D2),
|
'카테고리 관리',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.primaryColor,
|
||||||
),
|
),
|
||||||
body: Consumer<CategoryProvider>(
|
body: Consumer<CategoryProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
@@ -59,8 +65,11 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '카테고리 이름',
|
labelText: '카테고리 이름',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: AppColors.navyGray,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
@@ -72,20 +81,23 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedColor,
|
value: _selectedColor,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '색상 선택',
|
labelText: '색상 선택',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: AppColors.navyGray,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: '#1976D2', child: Text('파란색')),
|
value: '#1976D2', child: Text('파란색', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: '#4CAF50', child: Text('초록색')),
|
value: '#4CAF50', child: Text('초록색', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: '#FF9800', child: Text('주황색')),
|
value: '#FF9800', child: Text('주황색', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: '#F44336', child: Text('빨간색')),
|
value: '#F44336', child: Text('빨간색', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: '#9C27B0', child: Text('보라색')),
|
value: '#9C27B0', child: Text('보라색', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -96,19 +108,22 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedIcon,
|
value: _selectedIcon,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '아이콘 선택',
|
labelText: '아이콘 선택',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: AppColors.navyGray,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'subscriptions', child: Text('구독')),
|
value: 'subscriptions', child: Text('구독', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(value: 'movie', child: Text('영화')),
|
DropdownMenuItem(value: 'movie', child: Text('영화', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'music_note', child: Text('음악')),
|
value: 'music_note', child: Text('음악', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'fitness_center', child: Text('운동')),
|
value: 'fitness_center', child: Text('운동', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'shopping_cart', child: Text('쇼핑')),
|
value: 'shopping_cart', child: Text('쇼핑', style: TextStyle(color: AppColors.darkNavy))),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -119,7 +134,12 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _addCategory,
|
onPressed: _addCategory,
|
||||||
child: const Text('카테고리 추가'),
|
child: Text(
|
||||||
|
'카테고리 추가',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -141,7 +161,12 @@ class _CategoryManagementScreenState extends State<CategoryManagementScreen> {
|
|||||||
color: Color(
|
color: Color(
|
||||||
int.parse(category.color.replaceAll('#', '0xFF'))),
|
int.parse(category.color.replaceAll('#', '0xFF'))),
|
||||||
),
|
),
|
||||||
title: Text(category.name),
|
title: Text(
|
||||||
|
category.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.darkNavy,
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../widgets/detail/detail_form_section.dart';
|
|||||||
import '../widgets/detail/detail_event_section.dart';
|
import '../widgets/detail/detail_event_section.dart';
|
||||||
import '../widgets/detail/detail_url_section.dart';
|
import '../widgets/detail/detail_url_section.dart';
|
||||||
import '../widgets/detail/detail_action_buttons.dart';
|
import '../widgets/detail/detail_action_buttons.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 구독 상세 정보를 표시하고 편집할 수 있는 화면
|
/// 구독 상세 정보를 표시하고 편집할 수 있는 화면
|
||||||
class DetailScreen extends StatefulWidget {
|
class DetailScreen extends StatefulWidget {
|
||||||
@@ -46,7 +47,7 @@ class _DetailScreenState extends State<DetailScreen>
|
|||||||
final baseColor = _controller.getCardColor();
|
final baseColor = _controller.getCardColor();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F7),
|
backgroundColor: AppColors.backgroundColor,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _controller.scrollController,
|
controller: _controller.scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
@@ -175,11 +175,12 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFF10B981), // 초록색
|
backgroundColor: AppColors.successColor,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
top: MediaQuery.of(context).padding.top + 16, // 상단 여백
|
top: MediaQuery.of(context).padding.top + 16, // 상단 여백
|
||||||
@@ -219,19 +220,9 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final navigationProvider = context.watch<NavigationProvider>();
|
final navigationProvider = context.watch<NavigationProvider>();
|
||||||
final hour = DateTime.now().hour;
|
|
||||||
List<Color> backgroundGradient;
|
|
||||||
|
|
||||||
// 시간대별 배경 그라디언트 설정
|
// 메인 그라데이션 사용
|
||||||
if (hour >= 6 && hour < 10) {
|
List<Color> backgroundGradient = AppColors.mainGradient;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 인덱스가 유효한지 확인
|
// 현재 인덱스가 유효한지 확인
|
||||||
int currentIndex = navigationProvider.currentIndex;
|
int currentIndex = navigationProvider.currentIndex;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
import '../theme/adaptive_theme.dart';
|
import '../theme/adaptive_theme.dart';
|
||||||
import '../widgets/glassmorphism_card.dart';
|
import '../widgets/glassmorphism_card.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatelessWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
@@ -24,13 +25,13 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)
|
? AppColors.primaryColor.withValues(alpha: 0.2)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.primary
|
? AppColors.primaryColor
|
||||||
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -43,8 +44,8 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
? Icons.radio_button_checked
|
? Icons.radio_button_checked
|
||||||
: Icons.radio_button_unchecked,
|
: Icons.radio_button_unchecked,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.primary
|
? AppColors.primaryColor
|
||||||
: Theme.of(context).colorScheme.outline,
|
: AppColors.textSecondary,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@@ -54,8 +55,8 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).colorScheme.primary
|
? AppColors.primaryColor
|
||||||
: Theme.of(context).colorScheme.onSurface,
|
: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -187,8 +188,14 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
provider.setEnabled(true);
|
provider.setEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('알림 권한이 거부되었습니다'),
|
content: Text(
|
||||||
|
'알림 권한이 거부되었습니다',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.dangerColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -449,7 +456,15 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('스토어를 열 수 없습니다')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'스토어를 열 수 없습니다',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.dangerColor,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import '../services/subscription_url_matcher.dart';
|
|||||||
import 'package:intl/intl.dart'; // NumberFormat을 사용하기 위한 import 추가
|
import 'package:intl/intl.dart'; // NumberFormat을 사용하기 위한 import 추가
|
||||||
import '../widgets/glassmorphism_card.dart';
|
import '../widgets/glassmorphism_card.dart';
|
||||||
import '../widgets/themed_text.dart';
|
import '../widgets/themed_text.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||||
|
import '../widgets/common/buttons/primary_button.dart';
|
||||||
|
import '../widgets/common/buttons/secondary_button.dart';
|
||||||
|
import '../widgets/common/form_fields/base_text_field.dart';
|
||||||
|
|
||||||
class SmsScanScreen extends StatefulWidget {
|
class SmsScanScreen extends StatefulWidget {
|
||||||
const SmsScanScreen({super.key});
|
const SmsScanScreen({super.key});
|
||||||
@@ -352,41 +357,9 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
|
|
||||||
// 성공 메시지 표시
|
// 성공 메시지 표시
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showSuccess(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Row(
|
message: '${subscription.serviceName} 구독이 추가되었습니다.',
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,12 +368,9 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('구독 추가 중 오류 발생: $e');
|
print('구독 추가 중 오류 발생: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showError(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Text('구독 추가 중 오류가 발생했습니다: $e'),
|
message: '구독 추가 중 오류가 발생했습니다: $e',
|
||||||
backgroundColor: Colors.red,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 오류가 있어도 다음 구독으로 이동
|
// 오류가 있어도 다음 구독으로 이동
|
||||||
@@ -411,6 +381,16 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
|
|
||||||
// 현재 구독 건너뛰기
|
// 현재 구독 건너뛰기
|
||||||
void _skipCurrentSubscription() {
|
void _skipCurrentSubscription() {
|
||||||
|
final subscription = _scannedSubscriptions[_currentIndex];
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
AppSnackBar.showInfo(
|
||||||
|
context: context,
|
||||||
|
message: '${subscription.serviceName} 구독을 건너뛰었습니다.',
|
||||||
|
icon: Icons.skip_next_rounded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_moveToNextSubscription();
|
_moveToNextSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,12 +414,9 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
navigationProvider.updateCurrentIndex(0);
|
navigationProvider.updateCurrentIndex(0);
|
||||||
|
|
||||||
// 완료 메시지 표시
|
// 완료 메시지 표시
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppSnackBar.showSuccess(
|
||||||
const SnackBar(
|
context: context,
|
||||||
content: Text('모든 구독이 처리되었습니다.'),
|
message: '모든 구독이 처리되었습니다.',
|
||||||
backgroundColor: Colors.green,
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,15 +507,17 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
|
|
||||||
// 로딩 상태 UI
|
// 로딩 상태 UI
|
||||||
Widget _buildLoadingState() {
|
Widget _buildLoadingState() {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
CircularProgressIndicator(
|
||||||
SizedBox(height: 16),
|
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryColor),
|
||||||
ThemedText('SMS 메시지를 스캔 중입니다...'),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 16),
|
||||||
ThemedText('구독 서비스를 찾고 있습니다', opacity: 0.7),
|
const ThemedText('SMS 메시지를 스캔 중입니다...', forceDark: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const ThemedText('구독 서비스를 찾고 있습니다', opacity: 0.7, forceDark: true),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -564,6 +543,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'2회 이상 결제된 구독 서비스 찾기',
|
'2회 이상 결제된 구독 서비스 찾기',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Padding(
|
const Padding(
|
||||||
@@ -572,16 +552,17 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'문자 메시지를 스캔하여 반복적으로 결제된 구독 서비스를 자동으로 찾습니다. 서비스명과 금액을 추출하여 쉽게 구독을 추가할 수 있습니다.',
|
'문자 메시지를 스캔하여 반복적으로 결제된 구독 서비스를 자동으로 찾습니다. 서비스명과 금액을 추출하여 쉽게 구독을 추가할 수 있습니다.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton.icon(
|
PrimaryButton(
|
||||||
|
text: '스캔 시작하기',
|
||||||
|
icon: Icons.search_rounded,
|
||||||
onPressed: _scanSms,
|
onPressed: _scanSms,
|
||||||
icon: const Icon(Icons.search),
|
width: 200,
|
||||||
label: const Text('스캔 시작하기'),
|
height: 56,
|
||||||
style: ElevatedButton.styleFrom(
|
backgroundColor: AppColors.primaryColor,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -591,9 +572,10 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
// 구독 표시 상태 UI
|
// 구독 표시 상태 UI
|
||||||
Widget _buildSubscriptionState() {
|
Widget _buildSubscriptionState() {
|
||||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||||
return const Center(
|
// 처리 완료 후 초기 상태로 복귀
|
||||||
child: ThemedText('모든 구독 처리 완료'),
|
_scannedSubscriptions = [];
|
||||||
);
|
_currentIndex = 0;
|
||||||
|
return _buildInitialState(); // 스캔 버튼이 있는 초기 화면으로 돌아감
|
||||||
}
|
}
|
||||||
|
|
||||||
final subscription = _scannedSubscriptions[_currentIndex];
|
final subscription = _scannedSubscriptions[_currentIndex];
|
||||||
@@ -609,7 +591,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
// 진행 상태 표시
|
// 진행 상태 표시
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: (_currentIndex + 1) / _scannedSubscriptions.length,
|
value: (_currentIndex + 1) / _scannedSubscriptions.length,
|
||||||
backgroundColor: Colors.grey.withValues(alpha: 0.2),
|
backgroundColor: AppColors.navyGray.withValues(alpha: 0.2),
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).colorScheme.primary),
|
Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
@@ -618,6 +600,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'${_currentIndex + 1}/${_scannedSubscriptions.length}',
|
'${_currentIndex + 1}/${_scannedSubscriptions.length}',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
@@ -632,6 +615,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'다음 구독을 찾았습니다',
|
'다음 구독을 찾았습니다',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 서비스명
|
// 서비스명
|
||||||
@@ -639,12 +623,14 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'서비스명',
|
'서비스명',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ThemedText(
|
ThemedText(
|
||||||
subscription.serviceName,
|
subscription.serviceName,
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
@@ -659,6 +645,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'월 비용',
|
'월 비용',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ThemedText(
|
ThemedText(
|
||||||
@@ -675,6 +662,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
).format(subscription.monthlyCost),
|
).format(subscription.monthlyCost),
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -687,6 +675,7 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'반복 횟수',
|
'반복 횟수',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ThemedText(
|
ThemedText(
|
||||||
@@ -713,12 +702,14 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'결제 주기',
|
'결제 주기',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ThemedText(
|
ThemedText(
|
||||||
subscription.billingCycle,
|
subscription.billingCycle,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -731,12 +722,14 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
'결제일',
|
'결제일',
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
ThemedText(
|
ThemedText(
|
||||||
_getNextBillingText(subscription.nextBillingDate),
|
_getNextBillingText(subscription.nextBillingDate),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
forceDark: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -746,17 +739,18 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 웹사이트 URL 입력 필드 추가/수정
|
// 웹사이트 URL 입력 필드 추가/수정
|
||||||
Padding(
|
BaseTextField(
|
||||||
padding: const EdgeInsets.all(8.0),
|
controller: _websiteUrlController,
|
||||||
child: TextField(
|
label: '웹사이트 URL (자동 추출됨)',
|
||||||
controller: _websiteUrlController,
|
hintText: '웹사이트 URL을 수정하거나 비워두세요',
|
||||||
decoration: const InputDecoration(
|
prefixIcon: Icon(
|
||||||
labelText: '웹사이트 URL (자동 추출됨)',
|
Icons.language,
|
||||||
hintText: '웹사이트 URL을 수정하거나 비워두세요',
|
color: AppColors.navyGray,
|
||||||
prefixIcon: Icon(Icons.language),
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.darkNavy,
|
||||||
|
),
|
||||||
|
fillColor: AppColors.pureWhite.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
@@ -764,22 +758,18 @@ class _SmsScanScreenState extends State<SmsScanScreen> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: SecondaryButton(
|
||||||
|
text: '건너뛰기',
|
||||||
onPressed: _skipCurrentSubscription,
|
onPressed: _skipCurrentSubscription,
|
||||||
style: OutlinedButton.styleFrom(
|
height: 48,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
|
||||||
child: const Text('건너뛰기'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: PrimaryButton(
|
||||||
|
text: '추가하기',
|
||||||
onPressed: _addCurrentSubscription,
|
onPressed: _addCurrentSubscription,
|
||||||
style: ElevatedButton.styleFrom(
|
height: 48,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
|
||||||
child: const Text('추가하기'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
// 글래스모피즘 오버레이
|
// 글래스모피즘 오버레이
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.05),
|
color: AppColors.pureWhite.withValues(alpha: 0.05),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Stack(
|
Stack(
|
||||||
@@ -188,8 +188,8 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: RadialGradient(
|
gradient: RadialGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Colors.white.withValues(alpha: 0.1),
|
AppColors.pureWhite.withValues(alpha: 0.1),
|
||||||
Colors.white.withValues(alpha: 0.0),
|
AppColors.pureWhite.withValues(alpha: 0.0),
|
||||||
],
|
],
|
||||||
stops: const [0.2, 1.0],
|
stops: const [0.2, 1.0],
|
||||||
),
|
),
|
||||||
@@ -208,8 +208,8 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: RadialGradient(
|
gradient: RadialGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Colors.white.withValues(alpha: 0.07),
|
AppColors.pureWhite.withValues(alpha: 0.07),
|
||||||
Colors.white.withValues(alpha: 0.0),
|
AppColors.pureWhite.withValues(alpha: 0.0),
|
||||||
],
|
],
|
||||||
stops: const [0.4, 1.0],
|
stops: const [0.4, 1.0],
|
||||||
),
|
),
|
||||||
@@ -250,23 +250,22 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
Colors.white
|
AppColors.pureWhite
|
||||||
.withValues(alpha: 0.2),
|
.withValues(alpha: 0.2),
|
||||||
Colors.white
|
AppColors.pureWhite
|
||||||
.withValues(alpha: 0.1),
|
.withValues(alpha: 0.1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(30),
|
BorderRadius.circular(30),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.white
|
color: AppColors.pureWhite
|
||||||
.withValues(alpha: 0.3),
|
.withValues(alpha: 0.3),
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black
|
color: AppColors.shadowBlack,
|
||||||
.withValues(alpha: 0.1),
|
|
||||||
spreadRadius: 0,
|
spreadRadius: 0,
|
||||||
blurRadius: 30,
|
blurRadius: 30,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
@@ -323,12 +322,12 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
'SubManager',
|
'SubManager',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -349,11 +348,11 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
'구독 서비스 관리를 더 쉽게',
|
'구독 서비스 관리를 더 쉽게',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white70,
|
color: AppColors.pureWhite.withValues(alpha: 0.7),
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -374,17 +373,17 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
height: 60,
|
height: 60,
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.1),
|
color: AppColors.pureWhite.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color:
|
||||||
Colors.white.withValues(alpha: 0.2),
|
AppColors.pureWhite.withValues(alpha: 0.2),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Colors.white),
|
AppColors.pureWhite),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -401,11 +400,11 @@ class _SplashScreenState extends State<SplashScreen>
|
|||||||
padding: const EdgeInsets.only(bottom: 24.0),
|
padding: const EdgeInsets.only(bottom: 24.0),
|
||||||
child: FadeTransition(
|
child: FadeTransition(
|
||||||
opacity: _fadeAnimation,
|
opacity: _fadeAnimation,
|
||||||
child: const Text(
|
child: Text(
|
||||||
'© 2023 CClabs. All rights reserved.',
|
'© 2023 CClabs. All rights reserved.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.white60,
|
color: AppColors.pureWhite.withValues(alpha: 0.6),
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class AppColors {
|
class AppColors {
|
||||||
// 메인 컬러 (Metronic Tailwind 스타일)
|
// 메인 컬러 (Metronic Tailwind 스타일)
|
||||||
static const primaryColor = Color(0xFF3B82F6); // 메트로닉 블루
|
static const primaryColor = Color(0xFF2563EB); // 딥 블루
|
||||||
static const secondaryColor = Color(0xFF64748B); // 슬레이트 600
|
static const secondaryColor = Color(0xFF60A5FA); // 스카이 블루
|
||||||
static const successColor = Color(0xFF10B981); // 그린
|
static const successColor = Color(0xFF38BDF8); // 소프트 민트
|
||||||
static const infoColor = Color(0xFF6366F1); // 인디고
|
static const infoColor = Color(0xFF6366F1); // 인디고
|
||||||
static const warningColor = Color(0xFFF59E0B); // 앰버
|
static const warningColor = Color(0xFFF59E0B); // 앰버
|
||||||
static const dangerColor = Color(0xFFEF4444); // 레드
|
static const dangerColor = Color(0xFFF472B6); // 핑크 액센트
|
||||||
|
|
||||||
// 배경색
|
// 배경색
|
||||||
static const backgroundColor = Color(0xFFF1F5F9); // 슬레이트 100
|
static const backgroundColor = Color(0xFFF1F5F9); // 슬레이트 100
|
||||||
@@ -17,18 +17,24 @@ class AppColors {
|
|||||||
|
|
||||||
// 텍스트 컬러
|
// 텍스트 컬러
|
||||||
static const textPrimary = Color(0xFF1E293B); // 슬레이트 800
|
static const textPrimary = Color(0xFF1E293B); // 슬레이트 800
|
||||||
static const textSecondary = Color(0xFF64748B); // 슬레이트 600
|
static const darkNavy = Color(0xFF1E293B); // 메인 텍스트 (color.md 가이드)
|
||||||
static const textMuted = Color(0xFF94A3B8); // 슬레이트 400
|
static const textSecondary = Color(0xFF334155); // 네이비 그레이
|
||||||
|
static const navyGray = Color(0xFF334155); // 서브 텍스트 (color.md 가이드)
|
||||||
|
static const textMuted = Color(0xFF334155); // 네이비 그레이
|
||||||
static const textLight = Color(0xFFFFFFFF); // 화이트
|
static const textLight = Color(0xFFFFFFFF); // 화이트
|
||||||
|
static const pureWhite = Color(0xFFFFFFFF); // 버튼 텍스트용 (color.md 가이드)
|
||||||
|
|
||||||
// 보더 & 디바이더
|
// 보더 & 디바이더
|
||||||
static const borderColor = Color(0xFFE2E8F0); // 슬레이트 200
|
static const borderColor = Color(0xFFE2E8F0); // 슬레이트 200
|
||||||
static const dividerColor = Color(0xFFE2E8F0); // 슬레이트 200
|
static const dividerColor = Color(0xFFE2E8F0); // 슬레이트 200
|
||||||
|
|
||||||
|
// 그림자 (color.md 가이드)
|
||||||
|
static const shadowBlack = Color(0x14000000); // rgba(0,0,0,0.08) - 8% opacity
|
||||||
|
|
||||||
// 그라데이션 컬러 - 다양한 효과를 위한 조합
|
// 그라데이션 컬러 - 다양한 효과를 위한 조합
|
||||||
static const List<Color> blueGradient = [
|
static const List<Color> blueGradient = [
|
||||||
Color(0xFF3B82F6),
|
Color(0xFF2563EB), // 딥 블루
|
||||||
Color(0xFF2563EB)
|
Color(0xFF60A5FA) // 스카이 블루
|
||||||
];
|
];
|
||||||
static const List<Color> tealGradient = [
|
static const List<Color> tealGradient = [
|
||||||
Color(0xFF14B8A6),
|
Color(0xFF14B8A6),
|
||||||
@@ -48,10 +54,10 @@ class AppColors {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Glassmorphism 효과를 위한 색상
|
// Glassmorphism 효과를 위한 색상
|
||||||
static const glassSurface = Color(0x0FFFFFFF); // 매우 연한 흰색 (6% opacity)
|
static const glassSurface = Color(0x33FFFFFF); // 화이트 글래스 (20% opacity)
|
||||||
static const glassBackground = Color(0x1AFFFFFF); // 연한 흰색 (10% opacity)
|
static const glassBackground = Color(0x33FFFFFF); // 화이트 글래스 (20% opacity)
|
||||||
static const glassCard = Color(0x33FFFFFF); // 반투명 흰색 (20% opacity)
|
static const glassCard = Color(0x33FFFFFF); // 반투명 흰색 (20% opacity)
|
||||||
static const glassBorder = Color(0x4DFFFFFF); // 반투명 테두리 (30% opacity)
|
static const glassBorder = Color(0xFF2563EB); // 딥 블루 테두리
|
||||||
static const glassOverlay = Color(0x0D000000); // 연한 검정 오버레이 (5% opacity)
|
static const glassOverlay = Color(0x0D000000); // 연한 검정 오버레이 (5% opacity)
|
||||||
|
|
||||||
// 다크 모드용 Glassmorphism 색상
|
// 다크 모드용 Glassmorphism 색상
|
||||||
@@ -62,8 +68,8 @@ class AppColors {
|
|||||||
|
|
||||||
// 백드롭 블러 효과를 위한 그라디언트
|
// 백드롭 블러 효과를 위한 그라디언트
|
||||||
static const List<Color> glassGradient = [
|
static const List<Color> glassGradient = [
|
||||||
|
Color(0x33FFFFFF), // 20% white
|
||||||
Color(0x1AFFFFFF), // 10% white
|
Color(0x1AFFFFFF), // 10% white
|
||||||
Color(0x0FFFFFFF), // 6% white
|
|
||||||
];
|
];
|
||||||
|
|
||||||
static const List<Color> glassGradientDark = [
|
static const List<Color> glassGradientDark = [
|
||||||
@@ -71,6 +77,18 @@ class AppColors {
|
|||||||
Color(0x0F000000), // 6% black
|
Color(0x0F000000), // 6% black
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 메인 그라데이션
|
||||||
|
static const List<Color> mainGradient = [
|
||||||
|
Color(0xFF2563EB), // 딥 블루
|
||||||
|
Color(0xFF60A5FA), // 스카이 블루
|
||||||
|
Color(0xFFE0E7EF), // 라이트 그레이
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<Color> accentGradient = [
|
||||||
|
Color(0xFF38BDF8), // 소프트 민트
|
||||||
|
Color(0xFF60A5FA), // 스카이 블루
|
||||||
|
];
|
||||||
|
|
||||||
// 시간대별 배경 그라디언트
|
// 시간대별 배경 그라디언트
|
||||||
static const List<Color> morningGradient = [
|
static const List<Color> morningGradient = [
|
||||||
Color(0xFFFED7AA), // 따뜻한 오렌지
|
Color(0xFFFED7AA), // 따뜻한 오렌지
|
||||||
|
|||||||
@@ -17,22 +17,22 @@ class AppTheme {
|
|||||||
// 기본 배경색
|
// 기본 배경색
|
||||||
scaffoldBackgroundColor: AppColors.backgroundColor,
|
scaffoldBackgroundColor: AppColors.backgroundColor,
|
||||||
|
|
||||||
// 카드 스타일 - 부드러운 그림자, 둥근 모서리
|
// 카드 스타일 - 글래스모피즘 효과
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardTheme(
|
||||||
color: AppColors.cardColor,
|
color: AppColors.glassCard,
|
||||||
elevation: 1,
|
elevation: 0,
|
||||||
shadowColor: Colors.black.withValues(alpha: 0.04),
|
shadowColor: AppColors.shadowBlack,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
side: const BorderSide(color: AppColors.borderColor, width: 0.5),
|
side: const BorderSide(color: AppColors.glassBorder, width: 1),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 앱바 스타일 - 깔끔하고 투명한 디자인
|
// 앱바 스타일 - 글래스모피즘 디자인
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: AppColors.surfaceColor,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: AppColors.textPrimary,
|
foregroundColor: AppColors.textPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
@@ -43,7 +43,7 @@ class AppTheme {
|
|||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
),
|
),
|
||||||
iconTheme: const IconThemeData(
|
iconTheme: const IconThemeData(
|
||||||
color: AppColors.secondaryColor,
|
color: AppColors.primaryColor,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -52,21 +52,21 @@ class AppTheme {
|
|||||||
textTheme: const TextTheme(
|
textTheme: const TextTheme(
|
||||||
// 헤드라인 - 페이지 제목
|
// 헤드라인 - 페이지 제목
|
||||||
headlineLarge: const TextStyle(
|
headlineLarge: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
headlineMedium: const TextStyle(
|
headlineMedium: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
headlineSmall: const TextStyle(
|
headlineSmall: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: -0.25,
|
letterSpacing: -0.25,
|
||||||
@@ -75,21 +75,21 @@ class AppTheme {
|
|||||||
|
|
||||||
// 타이틀 - 카드, 섹션 제목
|
// 타이틀 - 카드, 섹션 제목
|
||||||
titleLarge: const TextStyle(
|
titleLarge: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
titleMedium: TextStyle(
|
titleMedium: TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: -0.1,
|
letterSpacing: -0.1,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
titleSmall: TextStyle(
|
titleSmall: TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
@@ -98,21 +98,21 @@ class AppTheme {
|
|||||||
|
|
||||||
// 본문 텍스트
|
// 본문 텍스트
|
||||||
bodyLarge: TextStyle(
|
bodyLarge: TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
letterSpacing: 0.1,
|
letterSpacing: 0.1,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
bodyMedium: TextStyle(
|
bodyMedium: TextStyle(
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
letterSpacing: 0.1,
|
letterSpacing: 0.1,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
bodySmall: TextStyle(
|
bodySmall: TextStyle(
|
||||||
color: AppColors.textMuted,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
@@ -121,21 +121,21 @@ class AppTheme {
|
|||||||
|
|
||||||
// 라벨 텍스트
|
// 라벨 텍스트
|
||||||
labelLarge: TextStyle(
|
labelLarge: TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0.1,
|
letterSpacing: 0.1,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
labelMedium: TextStyle(
|
labelMedium: TextStyle(
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
labelSmall: TextStyle(
|
labelSmall: TextStyle(
|
||||||
color: AppColors.textMuted,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
@@ -143,10 +143,10 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 입력 필드 스타일 - 깔끔하고 현대적인 디자인
|
// 입력 필드 스타일 - 글래스모피즘 디자인
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.surfaceColorAlt,
|
fillColor: AppColors.glassBackground,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -154,7 +154,7 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: AppColors.borderColor, width: 1),
|
borderSide: const BorderSide(color: AppColors.textSecondary, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -224,13 +224,13 @@ class AppTheme {
|
|||||||
// 아웃라인 버튼 스타일
|
// 아웃라인 버튼 스타일
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.textPrimary,
|
foregroundColor: AppColors.primaryColor,
|
||||||
minimumSize: const Size(0, 48),
|
minimumSize: const Size(0, 48),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
side: const BorderSide(color: AppColors.borderColor, width: 1),
|
side: const BorderSide(color: AppColors.secondaryColor, width: 1),
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -265,7 +265,7 @@ class AppTheme {
|
|||||||
}),
|
}),
|
||||||
trackColor: MaterialStateProperty.resolveWith<Color>((states) {
|
trackColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||||||
if (states.contains(MaterialState.selected)) {
|
if (states.contains(MaterialState.selected)) {
|
||||||
return AppColors.primaryColor.withValues(alpha: 0.5);
|
return AppColors.secondaryColor.withValues(alpha: 0.5);
|
||||||
}
|
}
|
||||||
return AppColors.borderColor;
|
return AppColors.borderColor;
|
||||||
}),
|
}),
|
||||||
@@ -282,7 +282,7 @@ class AppTheme {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
side: const BorderSide(color: AppColors.borderColor, width: 1.5),
|
side: const BorderSide(color: AppColors.secondaryColor, width: 1.5),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 라디오 버튼 스타일
|
// 라디오 버튼 스타일
|
||||||
@@ -291,16 +291,16 @@ class AppTheme {
|
|||||||
if (states.contains(MaterialState.selected)) {
|
if (states.contains(MaterialState.selected)) {
|
||||||
return AppColors.primaryColor;
|
return AppColors.primaryColor;
|
||||||
}
|
}
|
||||||
return AppColors.borderColor;
|
return AppColors.textSecondary;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 슬라이더 스타일
|
// 슬라이더 스타일
|
||||||
sliderTheme: SliderThemeData(
|
sliderTheme: SliderThemeData(
|
||||||
activeTrackColor: AppColors.primaryColor,
|
activeTrackColor: AppColors.primaryColor,
|
||||||
inactiveTrackColor: AppColors.borderColor,
|
inactiveTrackColor: AppColors.textSecondary,
|
||||||
thumbColor: AppColors.primaryColor,
|
thumbColor: AppColors.primaryColor,
|
||||||
overlayColor: AppColors.primaryColor.withValues(alpha: 0.2),
|
overlayColor: AppColors.primaryColor.withValues(alpha: 0.3),
|
||||||
trackHeight: 4,
|
trackHeight: 4,
|
||||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),
|
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),
|
||||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 20),
|
overlayShape: const RoundSliderOverlayShape(overlayRadius: 20),
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg
|
|||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(appBarOpacity),
|
color: Colors.white.withValues(alpha: appBarOpacity),
|
||||||
boxShadow: appBarOpacity > 0.6
|
boxShadow: appBarOpacity > 0.6
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1 * appBarOpacity),
|
color: Colors.black.withValues(alpha: 0.1 * appBarOpacity),
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
@@ -51,7 +51,7 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg
|
|||||||
shadows: appBarOpacity > 0.6
|
shadows: appBarOpacity > 0.6
|
||||||
? [
|
? [
|
||||||
Shadow(
|
Shadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
offset: const Offset(0, 1),
|
offset: const Offset(0, 1),
|
||||||
blurRadius: 2,
|
blurRadius: 2,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: controller.isEventActive
|
color: controller.isEventActive
|
||||||
? const Color(0xFF3B82F6)
|
? const Color(0xFF3B82F6)
|
||||||
: Colors.grey.withOpacity(0.2),
|
: Colors.grey.withValues(alpha: 0.2),
|
||||||
width: controller.isEventActive ? 2 : 1,
|
width: controller.isEventActive ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class _CurrencyOption extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? const Color(0xFF3B82F6)
|
? const Color(0xFF3B82F6)
|
||||||
: Colors.grey.withOpacity(0.1),
|
: Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -350,7 +350,7 @@ class _BillingCycleSelector extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? gradientColors[0]
|
? gradientColors[0]
|
||||||
: Colors.grey.withOpacity(0.1),
|
: Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -402,14 +402,14 @@ class _CategorySelector extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? gradientColors[0]
|
? gradientColors[0]
|
||||||
: Colors.grey.withOpacity(0.1),
|
: Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
category.emoji,
|
category.icon,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class AddSubscriptionHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: controller.gradientColors[0].withOpacity(0.3),
|
color: controller.gradientColors[0].withValues(alpha: 0.3),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: 0,
|
spreadRadius: 0,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
@@ -44,7 +44,7 @@ class AddSubscriptionHeader extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import '../../models/subscription_model.dart';
|
import '../../models/subscription_model.dart';
|
||||||
import '../../services/currency_util.dart';
|
import '../../services/currency_util.dart';
|
||||||
|
import '../../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 파이 차트에서 선택된 섹션에 표시되는 배지 위젯
|
/// 파이 차트에서 선택된 섹션에 표시되는 배지 위젯
|
||||||
class AnalysisBadge extends StatelessWidget {
|
class AnalysisBadge extends StatelessWidget {
|
||||||
@@ -23,7 +24,7 @@ class AnalysisBadge extends StatelessWidget {
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
@@ -31,7 +32,7 @@ class AnalysisBadge extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.5),
|
color: AppColors.shadowBlack,
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
spreadRadius: 2,
|
spreadRadius: 2,
|
||||||
),
|
),
|
||||||
@@ -48,7 +49,7 @@ class AnalysisBadge extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.black87,
|
color: AppColors.darkNavy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
@@ -68,7 +69,7 @@ class AnalysisBadge extends StatelessWidget {
|
|||||||
displayText,
|
displayText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
color: Colors.black54,
|
color: AppColors.navyGray,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/subscription_provider.dart';
|
import '../../providers/subscription_provider.dart';
|
||||||
import '../../services/currency_util.dart';
|
import '../../services/currency_util.dart';
|
||||||
|
import '../../theme/app_colors.dart';
|
||||||
import '../glassmorphism_card.dart';
|
import '../glassmorphism_card.dart';
|
||||||
import '../themed_text.dart';
|
import '../themed_text.dart';
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ class EventAnalysisCard extends StatelessWidget {
|
|||||||
const FaIcon(
|
const FaIcon(
|
||||||
FontAwesomeIcons.fire,
|
FontAwesomeIcons.fire,
|
||||||
size: 12,
|
size: 12,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -81,7 +82,7 @@ class EventAnalysisCard extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -159,10 +160,10 @@ class EventAnalysisCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.05),
|
color: AppColors.darkNavy.withValues(alpha: 0.05),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.white.withValues(alpha: 0.1),
|
color: AppColors.darkNavy.withValues(alpha: 0.1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -194,7 +195,7 @@ class EventAnalysisCard extends StatelessWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
decoration: TextDecoration
|
decoration: TextDecoration
|
||||||
.lineThrough,
|
.lineThrough,
|
||||||
color: Colors.grey,
|
color: AppColors.navyGray,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -205,7 +206,7 @@ class EventAnalysisCard extends StatelessWidget {
|
|||||||
const Icon(
|
const Icon(
|
||||||
Icons.arrow_forward,
|
Icons.arrow_forward,
|
||||||
size: 12,
|
size: 12,
|
||||||
color: Colors.grey,
|
color: AppColors.navyGray,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FutureBuilder<String>(
|
FutureBuilder<String>(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import '../../services/currency_util.dart';
|
import '../../services/currency_util.dart';
|
||||||
|
import '../../theme/app_colors.dart';
|
||||||
import '../glassmorphism_card.dart';
|
import '../glassmorphism_card.dart';
|
||||||
import '../themed_text.dart';
|
import '../themed_text.dart';
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
|||||||
backDrawRodData: BackgroundBarChartRodData(
|
backDrawRodData: BackgroundBarChartRodData(
|
||||||
show: true,
|
show: true,
|
||||||
toY: maxAmount + (maxAmount * 0.1),
|
toY: maxAmount + (maxAmount * 0.1),
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
color: AppColors.navyGray.withValues(alpha: 0.1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -124,7 +125,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
getDrawingHorizontalLine: (value) {
|
getDrawingHorizontalLine: (value) {
|
||||||
return FlLine(
|
return FlLine(
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
color: AppColors.navyGray.withValues(alpha: 0.1),
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -163,14 +164,14 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
|||||||
barTouchData: BarTouchData(
|
barTouchData: BarTouchData(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
touchTooltipData: BarTouchTooltipData(
|
touchTooltipData: BarTouchTooltipData(
|
||||||
tooltipBgColor: Colors.blueGrey.shade800,
|
tooltipBgColor: AppColors.darkNavy,
|
||||||
tooltipRoundedRadius: 8,
|
tooltipRoundedRadius: 8,
|
||||||
getTooltipItem:
|
getTooltipItem:
|
||||||
(group, groupIndex, rod, rodIndex) {
|
(group, groupIndex, rod, rodIndex) {
|
||||||
return BarTooltipItem(
|
return BarTooltipItem(
|
||||||
'${monthlyData[group.x]['monthName']}\n',
|
'${monthlyData[group.x]['monthName']}\n',
|
||||||
const TextStyle(
|
const TextStyle(
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -179,7 +180,7 @@ class MonthlyExpenseChartCard extends StatelessWidget {
|
|||||||
monthlyData[group.x]['totalExpense']
|
monthlyData[group.x]['totalExpense']
|
||||||
as double),
|
as double),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.yellow,
|
color: Color(0xFFFBBF24),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import '../../models/subscription_model.dart';
|
import '../../models/subscription_model.dart';
|
||||||
import '../../services/currency_util.dart';
|
import '../../services/currency_util.dart';
|
||||||
|
import '../../theme/app_colors.dart';
|
||||||
import '../glassmorphism_card.dart';
|
import '../glassmorphism_card.dart';
|
||||||
import '../themed_text.dart';
|
import '../themed_text.dart';
|
||||||
import 'analysis_badge.dart';
|
import 'analysis_badge.dart';
|
||||||
@@ -68,7 +69,7 @@ class SubscriptionPieChartCard extends StatelessWidget {
|
|||||||
titleStyle: TextStyle(
|
titleStyle: TextStyle(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
shadows: const [
|
shadows: const [
|
||||||
Shadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1))
|
Shadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1))
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class TotalExpenseSummaryCard extends StatelessWidget {
|
|||||||
child: const FaIcon(
|
child: const FaIcon(
|
||||||
FontAwesomeIcons.listCheck,
|
FontAwesomeIcons.listCheck,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.blue,
|
color: AppColors.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -181,7 +181,7 @@ class TotalExpenseSummaryCard extends StatelessWidget {
|
|||||||
child: const FaIcon(
|
child: const FaIcon(
|
||||||
FontAwesomeIcons.chartLine,
|
FontAwesomeIcons.chartLine,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.green,
|
color: AppColors.successColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 위험한 액션에 사용되는 Danger 버튼
|
/// 위험한 액션에 사용되는 Danger 버튼
|
||||||
/// 삭제, 취소, 종료 등의 위험한 액션에 사용됩니다.
|
/// 삭제, 취소, 종료 등의 위험한 액션에 사용됩니다.
|
||||||
@@ -39,7 +40,7 @@ class DangerButton extends StatefulWidget {
|
|||||||
class _DangerButtonState extends State<DangerButton> {
|
class _DangerButtonState extends State<DangerButton> {
|
||||||
bool _isHovered = false;
|
bool _isHovered = false;
|
||||||
|
|
||||||
static const Color _dangerColor = Color(0xFFDC2626);
|
static const Color _dangerColor = AppColors.dangerColor;
|
||||||
|
|
||||||
Future<void> _handlePress() async {
|
Future<void> _handlePress() async {
|
||||||
if (widget.requireConfirmation) {
|
if (widget.requireConfirmation) {
|
||||||
@@ -62,7 +63,7 @@ class _DangerButtonState extends State<DangerButton> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _dangerColor.withOpacity(0.1),
|
color: _dangerColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -98,7 +99,7 @@ class _DangerButtonState extends State<DangerButton> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.text,
|
widget.text,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: AppColors.pureWhite),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -126,14 +127,14 @@ class _DangerButtonState extends State<DangerButton> {
|
|||||||
onPressed: widget.onPressed != null ? _handlePress : null,
|
onPressed: widget.onPressed != null ? _handlePress : null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _dangerColor,
|
backgroundColor: _dangerColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.pureWhite,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
),
|
),
|
||||||
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
|
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
|
||||||
elevation: widget.enableHoverEffect && _isHovered ? 8 : 4,
|
elevation: widget.enableHoverEffect && _isHovered ? 2 : 0,
|
||||||
shadowColor: _dangerColor.withOpacity(0.5),
|
shadowColor: Colors.black.withValues(alpha: 0.08),
|
||||||
disabledBackgroundColor: _dangerColor.withOpacity(0.6),
|
disabledBackgroundColor: _dangerColor.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -142,7 +143,7 @@ class _DangerButtonState extends State<DangerButton> {
|
|||||||
if (widget.icon != null) ...[
|
if (widget.icon != null) ...[
|
||||||
Icon(
|
Icon(
|
||||||
widget.icon,
|
widget.icon,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
size: _isHovered ? 24 : 20,
|
size: _isHovered ? 24 : 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -152,7 +153,7 @@ class _DangerButtonState extends State<DangerButton> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: widget.fontSize,
|
fontSize: widget.fontSize,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 주요 액션에 사용되는 Primary 버튼
|
/// 주요 액션에 사용되는 Primary 버튼
|
||||||
/// 저장, 추가, 확인 등의 주요 액션에 사용됩니다.
|
/// 저장, 추가, 확인 등의 주요 액션에 사용됩니다.
|
||||||
@@ -43,7 +44,7 @@ class _PrimaryButtonState extends State<PrimaryButton> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final effectiveBackgroundColor = widget.backgroundColor ?? theme.primaryColor;
|
final effectiveBackgroundColor = widget.backgroundColor ?? theme.primaryColor;
|
||||||
final effectiveForegroundColor = widget.foregroundColor ?? Colors.white;
|
final effectiveForegroundColor = widget.foregroundColor ?? AppColors.pureWhite;
|
||||||
|
|
||||||
Widget button = AnimatedContainer(
|
Widget button = AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
@@ -61,9 +62,9 @@ class _PrimaryButtonState extends State<PrimaryButton> {
|
|||||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
),
|
),
|
||||||
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
|
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
|
||||||
elevation: widget.enableHoverEffect && _isHovered ? 8 : 4,
|
elevation: widget.enableHoverEffect && _isHovered ? 2 : 0,
|
||||||
shadowColor: effectiveBackgroundColor.withOpacity(0.5),
|
shadowColor: Colors.black.withValues(alpha: 0.08),
|
||||||
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.6),
|
disabledBackgroundColor: effectiveBackgroundColor.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
child: widget.isLoading
|
child: widget.isLoading
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 부차적인 액션에 사용되는 Secondary 버튼
|
/// 부차적인 액션에 사용되는 Secondary 버튼
|
||||||
/// 취소, 되돌아가기, 부가 옵션 등에 사용됩니다.
|
/// 취소, 되돌아가기, 부가 옵션 등에 사용됩니다.
|
||||||
@@ -42,10 +43,8 @@ class _SecondaryButtonState extends State<SecondaryButton> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final effectiveBorderColor = widget.borderColor ??
|
final effectiveBorderColor = widget.borderColor ?? AppColors.secondaryColor;
|
||||||
theme.colorScheme.onSurface.withOpacity(0.2);
|
final effectiveTextColor = widget.textColor ?? AppColors.primaryColor;
|
||||||
final effectiveTextColor = widget.textColor ??
|
|
||||||
theme.colorScheme.onSurface.withOpacity(0.8);
|
|
||||||
|
|
||||||
Widget button = AnimatedContainer(
|
Widget button = AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
@@ -63,7 +62,7 @@ class _SecondaryButtonState extends State<SecondaryButton> {
|
|||||||
),
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: _isHovered
|
color: _isHovered
|
||||||
? effectiveBorderColor.withOpacity(0.4)
|
? effectiveBorderColor.withValues(alpha: 0.4)
|
||||||
: effectiveBorderColor,
|
: effectiveBorderColor,
|
||||||
width: widget.borderWidth,
|
width: widget.borderWidth,
|
||||||
),
|
),
|
||||||
@@ -72,7 +71,7 @@ class _SecondaryButtonState extends State<SecondaryButton> {
|
|||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
backgroundColor: _isHovered
|
backgroundColor: _isHovered
|
||||||
? theme.colorScheme.onSurface.withOpacity(0.05)
|
? AppColors.glassBackground
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -142,13 +141,13 @@ class _TextLinkButtonState extends State<TextLinkButton> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final effectiveColor = widget.color ?? theme.colorScheme.primary;
|
final effectiveColor = widget.color ?? AppColors.primaryColor;
|
||||||
|
|
||||||
Widget button = AnimatedContainer(
|
Widget button = AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _isHovered
|
color: _isHovered
|
||||||
? theme.colorScheme.onSurface.withOpacity(0.05)
|
? theme.colorScheme.onSurface.withValues(alpha: 0.05)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class SectionCard extends StatelessWidget {
|
|||||||
final effectiveBackgroundColor = backgroundColor ?? Colors.white;
|
final effectiveBackgroundColor = backgroundColor ?? Colors.white;
|
||||||
final effectiveShadow = boxShadow ?? [
|
final effectiveShadow = boxShadow ?? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.05),
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -116,7 +116,7 @@ class TransparentSectionCard extends StatelessWidget {
|
|||||||
Widget card = Container(
|
Widget card = Container(
|
||||||
margin: margin,
|
margin: margin,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(opacity),
|
color: Colors.white.withValues(alpha: opacity),
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
border: borderColor != null
|
border: borderColor != null
|
||||||
? Border.all(color: borderColor!, width: 1)
|
? Border.all(color: borderColor!, width: 1)
|
||||||
@@ -134,7 +134,7 @@ class TransparentSectionCard extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white.withOpacity(0.9),
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -207,7 +207,7 @@ class InfoCard extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ConfirmationDialog extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (iconColor ?? effectiveConfirmColor).withOpacity(0.1),
|
color: (iconColor ?? effectiveConfirmColor).withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -163,7 +163,7 @@ class SuccessDialog extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.green.withOpacity(0.1),
|
color: Colors.green.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -271,7 +271,7 @@ class ErrorDialog extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red.withOpacity(0.1),
|
color: Colors.red.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class LoadingOverlay extends StatelessWidget {
|
|||||||
child,
|
child,
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
Container(
|
Container(
|
||||||
color: (backgroundColor ?? Colors.black).withOpacity(opacity),
|
color: (backgroundColor ?? Colors.black).withValues(alpha: opacity),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
@@ -36,7 +36,7 @@ class LoadingOverlay extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -193,7 +193,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
|
|||||||
width: widget.size / 5,
|
width: widget.size / 5,
|
||||||
height: widget.size / 5,
|
height: widget.size / 5,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: effectiveColor.withOpacity(0.3 + value * 0.7),
|
color: effectiveColor.withValues(alpha: 0.3 + value * 0.7),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -212,7 +212,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
|
|||||||
height: widget.size,
|
height: widget.size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: effectiveColor.withOpacity(0.3),
|
color: effectiveColor.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -220,7 +220,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
|
|||||||
height: widget.size * (0.3 + _animation.value * 0.5),
|
height: widget.size * (0.3 + _animation.value * 0.5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: effectiveColor.withOpacity(1 - _animation.value),
|
color: effectiveColor.withValues(alpha: 1 - _animation.value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 공통 텍스트 필드 위젯
|
/// 공통 텍스트 필드 위젯
|
||||||
/// 프로젝트 전체에서 일관된 스타일의 텍스트 입력 필드를 제공합니다.
|
/// 프로젝트 전체에서 일관된 스타일의 텍스트 입력 필드를 제공합니다.
|
||||||
@@ -68,7 +69,7 @@ class BaseTextField extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: theme.colorScheme.onSurface,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -91,18 +92,18 @@ class BaseTextField extends StatelessWidget {
|
|||||||
cursorColor: cursorColor ?? theme.primaryColor,
|
cursorColor: cursorColor ?? theme.primaryColor,
|
||||||
style: style ?? TextStyle(
|
style: style ?? TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: theme.colorScheme.onSurface,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: AppColors.textMuted,
|
||||||
),
|
),
|
||||||
prefixIcon: prefixIcon,
|
prefixIcon: prefixIcon,
|
||||||
prefixText: prefixText,
|
prefixText: prefixText,
|
||||||
suffixIcon: suffixIcon,
|
suffixIcon: suffixIcon,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: fillColor ?? Colors.white,
|
fillColor: fillColor ?? AppColors.glassBackground,
|
||||||
contentPadding: contentPadding ?? const EdgeInsets.all(16),
|
contentPadding: contentPadding ?? const EdgeInsets.all(16),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -117,7 +118,10 @@ class BaseTextField extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
disabledBorder: OutlineInputBorder(
|
disabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ class DatePickerField extends StatelessWidget {
|
|||||||
surface: Colors.white,
|
surface: Colors.white,
|
||||||
onSurface: Colors.black,
|
onSurface: Colors.black,
|
||||||
),
|
),
|
||||||
dialogBackgroundColor: Colors.white,
|
|
||||||
),
|
),
|
||||||
child: child!,
|
child: child!,
|
||||||
);
|
);
|
||||||
@@ -98,7 +97,7 @@ class DatePickerField extends StatelessWidget {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: enabled
|
color: enabled
|
||||||
? theme.colorScheme.onSurface
|
? theme.colorScheme.onSurface
|
||||||
: theme.colorScheme.onSurface.withOpacity(0.5),
|
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -106,8 +105,8 @@ class DatePickerField extends StatelessWidget {
|
|||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: enabled
|
color: enabled
|
||||||
? theme.colorScheme.onSurface.withOpacity(0.6)
|
? theme.colorScheme.onSurface.withValues(alpha: 0.6)
|
||||||
: theme.colorScheme.onSurface.withOpacity(0.3),
|
: theme.colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -214,7 +213,6 @@ class _DateRangeItem extends StatelessWidget {
|
|||||||
surface: Colors.white,
|
surface: Colors.white,
|
||||||
onSurface: Colors.black,
|
onSurface: Colors.black,
|
||||||
),
|
),
|
||||||
dialogBackgroundColor: Colors.white,
|
|
||||||
),
|
),
|
||||||
child: child!,
|
child: child!,
|
||||||
);
|
);
|
||||||
@@ -239,7 +237,7 @@ class _DateRangeItem extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -252,7 +250,7 @@ class _DateRangeItem extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: date != null
|
color: date != null
|
||||||
? theme.colorScheme.onSurface
|
? theme.colorScheme.onSurface
|
||||||
: theme.colorScheme.onSurface.withOpacity(0.4),
|
: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
272
lib/widgets/common/snackbar/app_snackbar.dart
Normal file
272
lib/widgets/common/snackbar/app_snackbar.dart
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
|
|
||||||
|
/// 앱 전체에서 사용되는 통합 스낵바
|
||||||
|
/// 성공, 에러, 정보 등 다양한 타입의 메시지를 표시합니다.
|
||||||
|
class AppSnackBar {
|
||||||
|
/// 성공 메시지를 표시합니다.
|
||||||
|
static void showSuccess({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
IconData icon = Icons.check_circle_rounded,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: AppColors.successColor,
|
||||||
|
iconColor: AppColors.pureWhite,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 에러 메시지를 표시합니다.
|
||||||
|
static void showError({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
IconData icon = Icons.error_rounded,
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: AppColors.dangerColor,
|
||||||
|
iconColor: AppColors.pureWhite,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 정보 메시지를 표시합니다.
|
||||||
|
static void showInfo({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
IconData icon = Icons.info_rounded,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: AppColors.primaryColor,
|
||||||
|
iconColor: AppColors.pureWhite,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 경고 메시지를 표시합니다.
|
||||||
|
static void showWarning({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
IconData icon = Icons.warning_amber_rounded,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: AppColors.warningColor,
|
||||||
|
iconColor: AppColors.pureWhite,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 커스텀 스낵바를 표시합니다.
|
||||||
|
static void showCustom({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
required IconData icon,
|
||||||
|
required Color backgroundColor,
|
||||||
|
Color iconColor = AppColors.pureWhite,
|
||||||
|
Color textColor = AppColors.pureWhite,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool showAtTop = true,
|
||||||
|
SnackBarAction? action,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
iconColor: iconColor,
|
||||||
|
textColor: textColor,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 내부적으로 스낵바를 표시하는 메서드
|
||||||
|
static void _show({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
required IconData icon,
|
||||||
|
required Color backgroundColor,
|
||||||
|
required Color iconColor,
|
||||||
|
required Color textColor,
|
||||||
|
required Duration duration,
|
||||||
|
required bool showAtTop,
|
||||||
|
SnackBarAction? action,
|
||||||
|
}) {
|
||||||
|
// 기존 스낵바 제거
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
|
||||||
|
// 새 스낵바 표시
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
// 아이콘
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: iconColor.withValues(alpha: 0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: iconColor,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// 메시지
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textColor,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: showAtTop
|
||||||
|
? EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top + 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: MediaQuery.of(context).size.height - 120,
|
||||||
|
)
|
||||||
|
: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
duration: duration,
|
||||||
|
dismissDirection: DismissDirection.horizontal,
|
||||||
|
action: action,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로딩 스낵바를 표시합니다. (자동으로 사라지지 않음)
|
||||||
|
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showLoading({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
// 기존 스낵바 제거
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
|
||||||
|
return ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
// 로딩 인디케이터
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
margin: const EdgeInsets.only(right: 12),
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 메시지
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.pureWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.primaryColor,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: showAtTop
|
||||||
|
? EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top + 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: MediaQuery.of(context).size.height - 120,
|
||||||
|
)
|
||||||
|
: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
duration: const Duration(days: 365), // 자동으로 사라지지 않음
|
||||||
|
dismissDirection: DismissDirection.none, // 스와이프로 닫을 수 없음
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 액션 버튼이 있는 스낵바를 표시합니다.
|
||||||
|
static void showWithAction({
|
||||||
|
required BuildContext context,
|
||||||
|
required String message,
|
||||||
|
required String actionLabel,
|
||||||
|
required VoidCallback onActionPressed,
|
||||||
|
IconData icon = Icons.info_rounded,
|
||||||
|
Color backgroundColor = AppColors.primaryColor,
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool showAtTop = true,
|
||||||
|
}) {
|
||||||
|
_show(
|
||||||
|
context: context,
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
iconColor: AppColors.pureWhite,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
duration: duration,
|
||||||
|
showAtTop: showAtTop,
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: actionLabel,
|
||||||
|
textColor: AppColors.pureWhite,
|
||||||
|
onPressed: onActionPressed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -240,7 +240,7 @@ class _CurrencyOption extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: Colors.grey.withOpacity(0.1),
|
: Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -291,7 +291,7 @@ class _BillingCycleSelector extends StatelessWidget {
|
|||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? baseColor : Colors.grey.withOpacity(0.1),
|
color: isSelected ? baseColor : Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -341,14 +341,14 @@ class _CategorySelector extends StatelessWidget {
|
|||||||
vertical: 10,
|
vertical: 10,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? baseColor : Colors.grey.withOpacity(0.1),
|
color: isSelected ? baseColor : Colors.grey.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
category.emoji,
|
category.icon,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
|
|||||||
182
lib/widgets/dialogs/delete_confirmation_dialog.dart
Normal file
182
lib/widgets/dialogs/delete_confirmation_dialog.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:ui';
|
||||||
|
import '../../theme/app_colors.dart';
|
||||||
|
import '../common/buttons/primary_button.dart';
|
||||||
|
import '../common/buttons/secondary_button.dart';
|
||||||
|
|
||||||
|
/// 삭제 확인 다이얼로그
|
||||||
|
/// 글래스모피즘 스타일의 삭제 확인 다이얼로그입니다.
|
||||||
|
class DeleteConfirmationDialog extends StatelessWidget {
|
||||||
|
final String serviceName;
|
||||||
|
|
||||||
|
const DeleteConfirmationDialog({
|
||||||
|
super.key,
|
||||||
|
required this.serviceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// 글래스모피즘 배경
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.glassCard.withValues(alpha: 0.8),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.glassBorder,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 아이콘
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete_forever_rounded,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 타이틀
|
||||||
|
const Text(
|
||||||
|
'구독 삭제',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 설명
|
||||||
|
RichText(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: '정말로 '),
|
||||||
|
TextSpan(
|
||||||
|
text: serviceName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' 구독을\n삭제하시겠습니까?'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// 경고 메시지
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withValues(alpha: 0.05),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.red.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: Colors.red.withValues(alpha: 0.8),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'이 작업은 되돌릴 수 없습니다',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 버튼들
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SecondaryButton(
|
||||||
|
text: '취소',
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: PrimaryButton(
|
||||||
|
text: '삭제',
|
||||||
|
icon: Icons.delete_rounded,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 삭제 확인 다이얼로그를 표시합니다.
|
||||||
|
static Future<bool> show({
|
||||||
|
required BuildContext context,
|
||||||
|
required String serviceName,
|
||||||
|
}) async {
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
barrierColor: Colors.black.withValues(alpha: 0.5),
|
||||||
|
builder: (context) => DeleteConfirmationDialog(
|
||||||
|
serviceName: serviceName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'glassmorphism_card.dart';
|
import 'glassmorphism_card.dart';
|
||||||
import 'themed_text.dart';
|
import 'themed_text.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
/// 구독이 없을 때 표시되는 빈 화면 위젯
|
/// 구독이 없을 때 표시되는 빈 화면 위젯
|
||||||
///
|
///
|
||||||
@@ -49,14 +50,14 @@ class EmptyStateWidget extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Color(0xFF3B82F6), Color(0xFF2563EB)],
|
colors: AppColors.blueGradient,
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFF3B82F6).withValues(alpha: 0.3),
|
color: AppColors.primaryColor.withValues(alpha: 0.3),
|
||||||
spreadRadius: 0,
|
spreadRadius: 0,
|
||||||
blurRadius: 16,
|
blurRadius: 16,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
@@ -66,7 +67,7 @@ class EmptyStateWidget extends StatelessWidget {
|
|||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.subscriptions_outlined,
|
Icons.subscriptions_outlined,
|
||||||
size: 48,
|
size: 48,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -100,7 +101,7 @@ class EmptyStateWidget extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
backgroundColor: const Color(0xFF3B82F6),
|
backgroundColor: AppColors.primaryColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
@@ -112,7 +113,7 @@ class EmptyStateWidget extends StatelessWidget {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import '../theme/app_colors.dart';
|
import '../theme/app_colors.dart';
|
||||||
import '../utils/haptic_feedback_helper.dart';
|
import '../utils/haptic_feedback_helper.dart';
|
||||||
@@ -82,7 +81,7 @@ class _ExpandableFabState extends State<ExpandableFab>
|
|||||||
animation: _expandAnimation,
|
animation: _expandAnimation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black.withValues(alpha: 0.3 * _expandAnimation.value),
|
color: AppColors.shadowBlack.withValues(alpha: 3.75 * _expandAnimation.value),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -118,7 +117,7 @@ class _ExpandableFabState extends State<ExpandableFab>
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
action.icon,
|
action.icon,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -176,6 +175,7 @@ class _ExpandableFabState extends State<ExpandableFab>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.darkNavy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -72,42 +72,58 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar>
|
|||||||
offset: Offset(0, 100 * (1 - _animation.value)),
|
offset: Offset(0, 100 * (1 - _animation.value)),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _animation.value,
|
opacity: _animation.value,
|
||||||
child: GlassmorphismCard(
|
child: Stack(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
children: [
|
||||||
borderRadius: 24,
|
// 차단 레이어 - 크기 명시
|
||||||
blur: 10.0,
|
Positioned.fill(
|
||||||
child: Row(
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
_NavigationItem(
|
borderRadius: BorderRadius.circular(24),
|
||||||
icon: Icons.home_rounded,
|
),
|
||||||
label: '홈',
|
|
||||||
isSelected: widget.selectedIndex == 0,
|
|
||||||
onTap: () => _onItemTapped(0),
|
|
||||||
),
|
),
|
||||||
_NavigationItem(
|
),
|
||||||
icon: Icons.analytics_rounded,
|
// 글래스모피즘 레이어
|
||||||
label: '분석',
|
GlassmorphismCard(
|
||||||
isSelected: widget.selectedIndex == 1,
|
padding:
|
||||||
onTap: () => _onItemTapped(1),
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
|
borderRadius: 24,
|
||||||
|
blur: 10.0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_NavigationItem(
|
||||||
|
icon: Icons.home_rounded,
|
||||||
|
label: '홈',
|
||||||
|
isSelected: widget.selectedIndex == 0,
|
||||||
|
onTap: () => _onItemTapped(0),
|
||||||
|
),
|
||||||
|
_NavigationItem(
|
||||||
|
icon: Icons.analytics_rounded,
|
||||||
|
label: '분석',
|
||||||
|
isSelected: widget.selectedIndex == 1,
|
||||||
|
onTap: () => _onItemTapped(1),
|
||||||
|
),
|
||||||
|
_AddButton(
|
||||||
|
onTap: () => _onItemTapped(2),
|
||||||
|
),
|
||||||
|
_NavigationItem(
|
||||||
|
icon: Icons.qr_code_scanner_rounded,
|
||||||
|
label: 'SMS',
|
||||||
|
isSelected: widget.selectedIndex == 3,
|
||||||
|
onTap: () => _onItemTapped(3),
|
||||||
|
),
|
||||||
|
_NavigationItem(
|
||||||
|
icon: Icons.settings_rounded,
|
||||||
|
label: '설정',
|
||||||
|
isSelected: widget.selectedIndex == 4,
|
||||||
|
onTap: () => _onItemTapped(4),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_AddButton(
|
),
|
||||||
onTap: () => _onItemTapped(2),
|
],
|
||||||
),
|
|
||||||
_NavigationItem(
|
|
||||||
icon: Icons.qr_code_scanner_rounded,
|
|
||||||
label: 'SMS',
|
|
||||||
isSelected: widget.selectedIndex == 3,
|
|
||||||
onTap: () => _onItemTapped(3),
|
|
||||||
),
|
|
||||||
_NavigationItem(
|
|
||||||
icon: Icons.settings_rounded,
|
|
||||||
label: '설정',
|
|
||||||
isSelected: widget.selectedIndex == 4,
|
|
||||||
onTap: () => _onItemTapped(4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -137,8 +153,6 @@ class _NavigationItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -147,7 +161,7 @@ class _NavigationItem extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? const Color(0xFF14B8A6).withValues(alpha: 0.1)
|
? AppColors.primaryColor.withValues(alpha: 0.1)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -158,9 +172,7 @@ class _NavigationItem extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
color: isSelected
|
color: isSelected ? AppColors.primaryColor : AppColors.navyGray,
|
||||||
? const Color(0xFF14B8A6)
|
|
||||||
: (isDarkMode ? Colors.white70 : AppColors.textSecondary),
|
|
||||||
size: isSelected ? 26 : 24,
|
size: isSelected ? 26 : 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -170,9 +182,7 @@ class _NavigationItem extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||||
color: isSelected
|
color: isSelected ? AppColors.primaryColor : AppColors.navyGray,
|
||||||
? const Color(0xFF14B8A6)
|
|
||||||
: (isDarkMode ? Colors.white70 : AppColors.textSecondary),
|
|
||||||
),
|
),
|
||||||
child: Text(label),
|
child: Text(label),
|
||||||
),
|
),
|
||||||
@@ -243,17 +253,17 @@ class _AddButtonState extends State<_AddButton>
|
|||||||
colors: AppColors.blueGradient,
|
colors: AppColors.blueGradient,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryColor.withValues(alpha: 0.3),
|
color: AppColors.shadowBlack,
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 4),
|
offset: Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.add_rounded,
|
Icons.add_rounded,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class GlassmorphicAppBar extends StatelessWidget implements PreferredSizeWidget
|
|||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: isDarkMode
|
color: isDarkMode
|
||||||
? AppColors.glassBorderDark.withValues(alpha: 0.3)
|
? AppColors.primaryColor.withValues(alpha: 0.3)
|
||||||
: AppColors.glassBorder.withValues(alpha: 0.3),
|
: AppColors.glassBorder.withValues(alpha: 0.5),
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -268,8 +268,8 @@ class GlassmorphicSliverAppBar extends StatelessWidget {
|
|||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: isDarkMode
|
color: isDarkMode
|
||||||
? AppColors.glassBorderDark.withValues(alpha: 0.3)
|
? AppColors.primaryColor.withValues(alpha: 0.3)
|
||||||
: AppColors.glassBorder.withValues(alpha: 0.3),
|
: AppColors.glassBorder.withValues(alpha: 0.5),
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -105,17 +105,8 @@ class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
|||||||
return widget.backgroundGradient!;
|
return widget.backgroundGradient!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시간대별 기본 그라디언트
|
// 디폴트 그라디언트
|
||||||
final hour = DateTime.now().hour;
|
return AppColors.mainGradient;
|
||||||
if (hour >= 6 && hour < 10) {
|
|
||||||
return AppColors.morningGradient;
|
|
||||||
} else if (hour >= 10 && hour < 17) {
|
|
||||||
return AppColors.dayGradient;
|
|
||||||
} else if (hour >= 17 && hour < 20) {
|
|
||||||
return AppColors.eveningGradient;
|
|
||||||
} else {
|
|
||||||
return AppColors.nightGradient;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -166,7 +157,11 @@ class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: gradientColors.map((color) => color.withValues(alpha: 0.1)).toList(),
|
colors: [
|
||||||
|
AppColors.backgroundColor,
|
||||||
|
...gradientColors.map((color) => color.withValues(alpha: 0.05)).toList(),
|
||||||
|
AppColors.backgroundColor,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -201,7 +196,7 @@ class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
|||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: WavePainter(
|
painter: WavePainter(
|
||||||
animation: _waveController,
|
animation: _waveController,
|
||||||
waveColor: AppColors.primaryColor.withValues(alpha: 0.1),
|
waveColor: AppColors.secondaryColor.withValues(alpha: 0.1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -244,7 +239,7 @@ class ParticlePainter extends CustomPainter {
|
|||||||
final progress = animation.value;
|
final progress = animation.value;
|
||||||
final y = (particle.y + progress * particle.speed) % 1.0;
|
final y = (particle.y + progress * particle.speed) % 1.0;
|
||||||
|
|
||||||
paint.color = Colors.white.withValues(alpha: particle.opacity);
|
paint.color = AppColors.pureWhite.withValues(alpha: particle.opacity);
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
Offset(particle.x * size.width, y * size.height),
|
Offset(particle.x * size.width, y * size.height),
|
||||||
particle.size,
|
particle.size,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import '../theme/app_colors.dart';
|
import '../theme/app_colors.dart';
|
||||||
|
import 'themed_text.dart';
|
||||||
|
|
||||||
class GlassmorphismCard extends StatelessWidget {
|
class GlassmorphismCard extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -54,9 +55,7 @@ class GlassmorphismCard extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor ?? (isDarkMode
|
color: backgroundColor ?? AppColors.glassCard,
|
||||||
? AppColors.glassCardDark
|
|
||||||
: AppColors.glassCard),
|
|
||||||
gradient: gradient ?? LinearGradient(
|
gradient: gradient ?? LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
@@ -67,20 +66,22 @@ class GlassmorphismCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
border: border ?? Border.all(
|
border: border ?? Border.all(
|
||||||
color: isDarkMode
|
color: isDarkMode
|
||||||
? AppColors.glassBorderDark
|
? AppColors.primaryColor.withValues(alpha: 0.3)
|
||||||
: AppColors.glassBorder,
|
: AppColors.glassBorder,
|
||||||
width: 1.5,
|
width: 1,
|
||||||
),
|
),
|
||||||
boxShadow: boxShadow ?? [
|
boxShadow: boxShadow ?? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
color: AppColors.shadowBlack, // color.md 가이드: rgba(0,0,0,0.08)
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: -5,
|
spreadRadius: -5,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: child,
|
child: GlassmorphicIndicator(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -39,17 +39,15 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
child: GlassmorphismCard(
|
child: GlassmorphismCard(
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
blur: 15,
|
blur: 15,
|
||||||
backgroundColor: AppColors.primaryColor.withValues(alpha: 0.2),
|
backgroundColor: AppColors.glassCard,
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: AppColors.mainGradient.map((color) => color.withValues(alpha: 0.2)).toList(),
|
||||||
AppColors.primaryColor.withValues(alpha: 0.3),
|
),
|
||||||
AppColors.primaryColor.withBlue(
|
border: Border.all(
|
||||||
(AppColors.primaryColor.blue * 1.3)
|
color: AppColors.glassBorder,
|
||||||
.clamp(0, 255)
|
width: 1,
|
||||||
.toInt()).withValues(alpha: 0.2),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -81,7 +79,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'이번 달 총 구독 비용',
|
'이번 달 총 구독 비용',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -98,7 +96,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
decimalDigits: 0,
|
decimalDigits: 0,
|
||||||
).format(monthlyCost),
|
).format(monthlyCost),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: -1,
|
letterSpacing: -1,
|
||||||
@@ -108,7 +106,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'원',
|
'원',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -149,7 +147,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.white.withValues(alpha: 0.3),
|
color: AppColors.primaryColor.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -165,7 +163,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.local_offer_rounded,
|
Icons.local_offer_rounded,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Colors.white,
|
color: AppColors.primaryColor, // color.md 가이드: 밝은 배경 위 딥 블루 아이콘
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -175,7 +173,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'이벤트 할인 중',
|
'이벤트 할인 중',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -190,7 +188,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
decimalDigits: 0,
|
decimalDigits: 0,
|
||||||
).format(eventSavings),
|
).format(eventSavings),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: AppColors.primaryColor, // color.md 가이드: 밝은 배경 위 딥 블루 강조
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -198,7 +196,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
' 절약 ($activeEvents개)',
|
' 절약 ($activeEvents개)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.85),
|
color: AppColors.navyGray, // color.md 가이드: 밝은 배경 위 서브 텍스트
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -229,7 +227,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.15),
|
color: AppColors.glassBackground,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -238,7 +236,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.85),
|
color: AppColors.navyGray, // color.md 가이드: 밝은 배경 위 서브 텍스트
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -247,7 +245,7 @@ class MainScreenSummaryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/physics.dart';
|
|
||||||
|
|
||||||
/// 물리 기반 스프링 애니메이션을 적용하는 위젯
|
/// 물리 기반 스프링 애니메이션을 적용하는 위젯
|
||||||
class SpringAnimationWidget extends StatefulWidget {
|
class SpringAnimationWidget extends StatefulWidget {
|
||||||
@@ -212,7 +211,7 @@ class _GravityAnimationState extends State<GravityAnimation>
|
|||||||
late AnimationController _controller;
|
late AnimationController _controller;
|
||||||
double _position = 0;
|
double _position = 0;
|
||||||
double _velocity = 0;
|
double _velocity = 0;
|
||||||
double _floor = 300;
|
final double _floor = 300;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|||||||
@@ -190,14 +190,10 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getCardColor() {
|
|
||||||
return Colors.white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isNearBilling = _isNearBilling();
|
final isNearBilling = _isNearBilling();
|
||||||
final Color cardColor = _getCardColor();
|
|
||||||
|
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'subscription_${widget.subscription.id}',
|
tag: 'subscription_${widget.subscription.id}',
|
||||||
@@ -225,27 +221,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
blur: _isHovering ? 15 : 10,
|
blur: _isHovering ? 15 : 10,
|
||||||
child: Container(
|
width: double.infinity, // 전체 너비를 차지하도록 설정
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: _isHovering
|
|
||||||
? AppColors.primaryColor.withValues(alpha: 0.3)
|
|
||||||
: AppColors.borderColor,
|
|
||||||
width: _isHovering ? 1.5 : 0.5,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColors.primaryColor.withValues(alpha:
|
|
||||||
0.03 + (0.05 * _hoverController.value)),
|
|
||||||
blurRadius: 8 + (8 * _hoverController.value),
|
|
||||||
spreadRadius: 0,
|
|
||||||
offset: Offset(0, 4 + (2 * _hoverController.value)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 그라데이션 상단 바 효과
|
// 그라데이션 상단 바 효과
|
||||||
@@ -300,7 +276,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: Color(0xFF1E293B),
|
color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -334,7 +310,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.local_offer_rounded,
|
Icons.local_offer_rounded,
|
||||||
size: 11,
|
size: 11,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
SizedBox(width: 3),
|
SizedBox(width: 3),
|
||||||
Text(
|
Text(
|
||||||
@@ -342,7 +318,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: AppColors.pureWhite,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -371,7 +347,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -409,7 +385,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
decoration: TextDecoration.lineThrough,
|
decoration: TextDecoration.lineThrough,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -539,7 +515,7 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
'${widget.subscription.eventEndDate!.difference(DateTime.now()).inDays}일 남음',
|
'${widget.subscription.eventEndDate!.difference(DateTime.now()).inDays}일 남음',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.navyGray, // color.md 가이드: 서브 텍스트
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -555,7 +531,6 @@ class _SubscriptionCardState extends State<SubscriptionCard>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import '../widgets/staggered_list_animation.dart';
|
|||||||
import '../widgets/app_navigator.dart';
|
import '../widgets/app_navigator.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../providers/subscription_provider.dart';
|
import '../providers/subscription_provider.dart';
|
||||||
|
import './dialogs/delete_confirmation_dialog.dart';
|
||||||
|
import './common/snackbar/app_snackbar.dart';
|
||||||
|
|
||||||
/// 카테고리별로 구독 목록을 표시하는 위젯
|
/// 카테고리별로 구독 목록을 표시하는 위젯
|
||||||
class SubscriptionListWidget extends StatelessWidget {
|
class SubscriptionListWidget extends StatelessWidget {
|
||||||
@@ -92,14 +94,30 @@ class SubscriptionListWidget extends StatelessWidget {
|
|||||||
AppNavigator.toDetail(context, subscriptions[subIndex]);
|
AppNavigator.toDetail(context, subscriptions[subIndex]);
|
||||||
},
|
},
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
// 삭제 확인 다이얼로그
|
// 삭제 확인 다이얼로그 표시
|
||||||
final provider = Provider.of<SubscriptionProvider>(
|
final shouldDelete = await DeleteConfirmationDialog.show(
|
||||||
context,
|
context: context,
|
||||||
listen: false,
|
serviceName: subscriptions[subIndex].serviceName,
|
||||||
);
|
|
||||||
await provider.deleteSubscription(
|
|
||||||
subscriptions[subIndex].id,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (shouldDelete && context.mounted) {
|
||||||
|
// 사용자가 확인한 경우에만 삭제 진행
|
||||||
|
final provider = Provider.of<SubscriptionProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
await provider.deleteSubscription(
|
||||||
|
subscriptions[subIndex].id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
AppSnackBar.showSuccess(
|
||||||
|
context: context,
|
||||||
|
message: '${subscriptions[subIndex].serviceName} 구독이 삭제되었습니다.',
|
||||||
|
icon: Icons.delete_forever_rounded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import '../models/subscription_model.dart';
|
import '../models/subscription_model.dart';
|
||||||
import '../utils/haptic_feedback_helper.dart';
|
import '../utils/haptic_feedback_helper.dart';
|
||||||
import 'subscription_card.dart';
|
import 'subscription_card.dart';
|
||||||
import '../theme/app_colors.dart';
|
|
||||||
|
|
||||||
class SwipeableSubscriptionCard extends StatefulWidget {
|
class SwipeableSubscriptionCard extends StatefulWidget {
|
||||||
final SubscriptionModel subscription;
|
final SubscriptionModel subscription;
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
final VoidCallback? onDelete;
|
final Future<void> Function()? onDelete;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const SwipeableSubscriptionCard({
|
const SwipeableSubscriptionCard({
|
||||||
@@ -27,12 +26,15 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
late AnimationController _controller;
|
late AnimationController _controller;
|
||||||
late Animation<double> _animation;
|
late Animation<double> _animation;
|
||||||
double _dragStartX = 0;
|
double _dragStartX = 0;
|
||||||
double _dragExtent = 0;
|
double _currentOffset = 0; // 현재 카드의 실제 위치
|
||||||
|
bool _isDragging = false; // 드래그 중인지 여부
|
||||||
bool _isSwipingLeft = false;
|
bool _isSwipingLeft = false;
|
||||||
bool _hapticTriggered = false;
|
bool _hapticTriggered = false;
|
||||||
|
double _screenWidth = 0;
|
||||||
|
double _cardWidth = 0; // 카드의 실제 너비 (margin 제외)
|
||||||
|
|
||||||
static const double _swipeThreshold = 80.0;
|
static const double _actionThresholdPercent = 0.15; // 15%에서 액션 버튼 표시
|
||||||
static const double _deleteThreshold = 150.0;
|
static const double _deleteThresholdPercent = 0.40; // 40%에서 삭제/편집 실행
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -48,81 +50,137 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
parent: _controller,
|
parent: _controller,
|
||||||
curve: Curves.easeOutExpo,
|
curve: Curves.easeOutExpo,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// 애니메이션 상태 리스너 추가
|
||||||
|
_controller.addStatusListener(_onAnimationStatusChanged);
|
||||||
|
|
||||||
|
// 애니메이션 리스너 추가
|
||||||
|
_controller.addListener(_onAnimationUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
_cardWidth = _screenWidth - 32; // 좌우 margin 16px씩 제외
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(SwipeableSubscriptionCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// 위젯이 업데이트될 때 카드를 원위치로 복귀
|
||||||
|
if (oldWidget.subscription.id != widget.subscription.id) {
|
||||||
|
_controller.stop();
|
||||||
|
setState(() {
|
||||||
|
_currentOffset = 0;
|
||||||
|
_isDragging = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_controller.removeListener(_onAnimationUpdate);
|
||||||
|
_controller.removeStatusListener(_onAnimationStatusChanged);
|
||||||
|
_controller.stop();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onAnimationUpdate() {
|
||||||
|
if (!_isDragging) {
|
||||||
|
setState(() {
|
||||||
|
_currentOffset = _animation.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed && !_isDragging) {
|
||||||
|
setState(() {
|
||||||
|
_currentOffset = _animation.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleDragStart(DragStartDetails details) {
|
void _handleDragStart(DragStartDetails details) {
|
||||||
_dragStartX = details.localPosition.dx;
|
_dragStartX = details.localPosition.dx;
|
||||||
_hapticTriggered = false;
|
_hapticTriggered = false;
|
||||||
|
_isDragging = true;
|
||||||
|
_controller.stop(); // 진행 중인 애니메이션 중지
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragUpdate(DragUpdateDetails details) {
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
final delta = details.localPosition.dx - _dragStartX;
|
final delta = details.localPosition.dx - _dragStartX;
|
||||||
setState(() {
|
setState(() {
|
||||||
_dragExtent = delta;
|
_currentOffset = delta;
|
||||||
_isSwipingLeft = delta < 0;
|
_isSwipingLeft = delta < 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 햅틱 피드백 트리거
|
// 햅틱 피드백 트리거 (카드 너비의 15%)
|
||||||
if (!_hapticTriggered && _dragExtent.abs() > _swipeThreshold) {
|
final actionThreshold = _cardWidth * _actionThresholdPercent;
|
||||||
|
if (!_hapticTriggered && _currentOffset.abs() > actionThreshold) {
|
||||||
_hapticTriggered = true;
|
_hapticTriggered = true;
|
||||||
HapticFeedbackHelper.mediumImpact();
|
HapticFeedbackHelper.mediumImpact();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 삭제 임계값에 도달했을 때 강한 햅틱
|
// 삭제 임계값에 도달했을 때 강한 햅틱 (카드 너비의 40%)
|
||||||
if (_dragExtent.abs() > _deleteThreshold && _hapticTriggered) {
|
final deleteThreshold = _cardWidth * _deleteThresholdPercent;
|
||||||
|
if (_currentOffset.abs() > deleteThreshold && _hapticTriggered) {
|
||||||
HapticFeedbackHelper.heavyImpact();
|
HapticFeedbackHelper.heavyImpact();
|
||||||
_hapticTriggered = false; // 반복 방지
|
_hapticTriggered = false; // 반복 방지
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragEnd(DragEndDetails details) {
|
void _handleDragEnd(DragEndDetails details) async {
|
||||||
|
_isDragging = false;
|
||||||
final velocity = details.velocity.pixelsPerSecond.dx;
|
final velocity = details.velocity.pixelsPerSecond.dx;
|
||||||
final extent = _dragExtent.abs();
|
final extent = _currentOffset.abs();
|
||||||
|
|
||||||
if (extent > _deleteThreshold || velocity.abs() > 800) {
|
// 카드 너비의 40% 계산
|
||||||
// 삭제 액션
|
final deleteThreshold = _cardWidth * _deleteThresholdPercent;
|
||||||
|
|
||||||
|
if (extent > deleteThreshold || velocity.abs() > 800) {
|
||||||
|
// 40% 이상 스와이프 시 삭제/편집 액션
|
||||||
if (_isSwipingLeft && widget.onDelete != null) {
|
if (_isSwipingLeft && widget.onDelete != null) {
|
||||||
HapticFeedbackHelper.success();
|
HapticFeedbackHelper.success();
|
||||||
_animateToOffset(-MediaQuery.of(context).size.width);
|
// 삭제 확인 다이얼로그 표시
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
await widget.onDelete!();
|
||||||
widget.onDelete!();
|
// 다이얼로그가 닫힌 후 원위치로 복귀
|
||||||
});
|
if (mounted) {
|
||||||
|
_animateToOffset(0);
|
||||||
|
}
|
||||||
} else if (!_isSwipingLeft && widget.onEdit != null) {
|
} else if (!_isSwipingLeft && widget.onEdit != null) {
|
||||||
HapticFeedbackHelper.success();
|
HapticFeedbackHelper.success();
|
||||||
_animateToOffset(MediaQuery.of(context).size.width);
|
// 편집 화면으로 이동 전 원위치로 복귀
|
||||||
|
_animateToOffset(0);
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
widget.onEdit!();
|
widget.onEdit!();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// 액션이 없는 경우 원위치로 복귀
|
||||||
|
_animateToOffset(0);
|
||||||
}
|
}
|
||||||
} else if (extent > _swipeThreshold) {
|
|
||||||
// 액션 버튼 표시
|
|
||||||
HapticFeedbackHelper.lightImpact();
|
|
||||||
_animateToOffset(_isSwipingLeft ? -_swipeThreshold : _swipeThreshold);
|
|
||||||
} else {
|
} else {
|
||||||
// 원위치로 복귀
|
// 40% 미만: 모두 원위치로 복귀
|
||||||
_animateToOffset(0);
|
_animateToOffset(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _animateToOffset(double offset) {
|
void _animateToOffset(double offset) {
|
||||||
|
// 애니메이션 컨트롤러 리셋
|
||||||
|
_controller.stop();
|
||||||
|
_controller.value = 0;
|
||||||
|
|
||||||
_animation = Tween<double>(
|
_animation = Tween<double>(
|
||||||
begin: _dragExtent,
|
begin: _currentOffset,
|
||||||
end: offset,
|
end: offset,
|
||||||
).animate(CurvedAnimation(
|
).animate(CurvedAnimation(
|
||||||
parent: _controller,
|
parent: _controller,
|
||||||
curve: Curves.easeOutExpo,
|
curve: Curves.easeOutExpo,
|
||||||
));
|
));
|
||||||
_controller.forward(from: 0).then((_) {
|
|
||||||
setState(() {
|
_controller.forward();
|
||||||
_dragExtent = offset;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -135,9 +193,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
color: _isSwipingLeft
|
color: Colors.transparent, // 투명하게 변경
|
||||||
? AppColors.dangerColor
|
|
||||||
: AppColors.primaryColor,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -148,10 +204,10 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
padding: const EdgeInsets.only(left: 24),
|
padding: const EdgeInsets.only(left: 24),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
opacity: _dragExtent > 40 ? 1.0 : 0.0,
|
opacity: _currentOffset > (_cardWidth * 0.10) ? 1.0 : 0.0,
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
scale: _dragExtent > 40 ? 1.0 : 0.5,
|
scale: _currentOffset > (_cardWidth * 0.10) ? 1.0 : 0.5,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.edit_rounded,
|
Icons.edit_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -166,12 +222,12 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
padding: const EdgeInsets.only(right: 24),
|
padding: const EdgeInsets.only(right: 24),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
opacity: _dragExtent.abs() > 40 ? 1.0 : 0.0,
|
opacity: _currentOffset.abs() > (_cardWidth * 0.10) ? 1.0 : 0.0,
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
scale: _dragExtent.abs() > 40 ? 1.0 : 0.5,
|
scale: _currentOffset.abs() > (_cardWidth * 0.10) ? 1.0 : 0.5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_dragExtent.abs() > _deleteThreshold
|
_currentOffset.abs() > (_cardWidth * _deleteThresholdPercent)
|
||||||
? Icons.delete_forever_rounded
|
? Icons.delete_forever_rounded
|
||||||
: Icons.delete_rounded,
|
: Icons.delete_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -186,33 +242,24 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
),
|
),
|
||||||
|
|
||||||
// 스와이프 가능한 카드
|
// 스와이프 가능한 카드
|
||||||
AnimatedBuilder(
|
GestureDetector(
|
||||||
animation: _animation,
|
onHorizontalDragStart: _handleDragStart,
|
||||||
builder: (context, child) {
|
onHorizontalDragUpdate: _handleDragUpdate,
|
||||||
return Transform.translate(
|
onHorizontalDragEnd: _handleDragEnd,
|
||||||
offset: Offset(_animation.value, 0),
|
child: Transform.translate(
|
||||||
child: child,
|
offset: Offset(_currentOffset, 0),
|
||||||
);
|
child: Transform.scale(
|
||||||
},
|
scale: 1.0 - (_currentOffset.abs() / 2000),
|
||||||
child: GestureDetector(
|
child: Transform.rotate(
|
||||||
onHorizontalDragStart: _handleDragStart,
|
angle: _currentOffset / 2000,
|
||||||
onHorizontalDragUpdate: _handleDragUpdate,
|
child: GestureDetector(
|
||||||
onHorizontalDragEnd: _handleDragEnd,
|
onTap: () {
|
||||||
child: Transform.translate(
|
if (_currentOffset.abs() < 10) {
|
||||||
offset: Offset(_dragExtent, 0),
|
widget.onTap?.call();
|
||||||
child: Transform.scale(
|
}
|
||||||
scale: 1.0 - (_dragExtent.abs() / 2000),
|
},
|
||||||
child: Transform.rotate(
|
child: SubscriptionCard(
|
||||||
angle: _dragExtent / 2000,
|
subscription: widget.subscription,
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (_dragExtent.abs() < 10) {
|
|
||||||
widget.onTap?.call();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: SubscriptionCard(
|
|
||||||
subscription: widget.subscription,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -39,22 +39,20 @@ class ThemedText extends StatelessWidget {
|
|||||||
bool forceLight = false,
|
bool forceLight = false,
|
||||||
bool forceDark = false,
|
bool forceDark = false,
|
||||||
}) {
|
}) {
|
||||||
if (forceLight) return Colors.white;
|
if (forceLight) return AppColors.pureWhite;
|
||||||
if (forceDark) return AppColors.textPrimary;
|
if (forceDark) return AppColors.darkNavy;
|
||||||
|
|
||||||
final brightness = Theme.of(context).brightness;
|
final brightness = Theme.of(context).brightness;
|
||||||
|
|
||||||
// 글래스모피즘 환경에서는 보통 어두운 배경 위에 밝은 텍스트
|
// 글래스모피즘 환경에서는 배경이 밝으므로 어두운 텍스트 사용
|
||||||
if (_isGlassmorphicContext(context)) {
|
if (_isGlassmorphicContext(context)) {
|
||||||
return brightness == Brightness.dark
|
return AppColors.darkNavy; // color.md 가이드: 밝은 배경 위 어두운 텍스트
|
||||||
? Colors.white.withValues(alpha: 0.95)
|
|
||||||
: AppColors.textPrimary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 환경
|
// 일반 환경
|
||||||
return brightness == Brightness.dark
|
return brightness == Brightness.dark
|
||||||
? Colors.white
|
? AppColors.pureWhite
|
||||||
: AppColors.textPrimary;
|
: AppColors.darkNavy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 글래스모피즘 컨텍스트인지 확인
|
/// 글래스모피즘 컨텍스트인지 확인
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
key: ValueKey('local_logo_${_localLogoPath}'),
|
key: ValueKey('local_logo_$_localLogoPath'),
|
||||||
borderRadius: BorderRadius.circular(widget.size * 0.2),
|
borderRadius: BorderRadius.circular(widget.size * 0.2),
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(_localLogoPath!),
|
File(_localLogoPath!),
|
||||||
|
|||||||
Reference in New Issue
Block a user