Files
submanager/lib/screens/settings_screen.dart
JiWoong Sul 4731288622 Major UI/UX and architecture improvements
- Implemented new navigation system with NavigationProvider and route management
- Added adaptive theme system with ThemeProvider for better theme handling
- Introduced glassmorphism design elements (app bars, scaffolds, cards)
- Added advanced animations (spring animations, page transitions, staggered lists)
- Implemented performance optimizations (memory manager, lazy loading)
- Refactored Analysis screen into modular components
- Added floating navigation bar with haptic feedback
- Improved subscription cards with swipe actions
- Enhanced skeleton loading with better animations
- Added cached network image support
- Improved overall app architecture and code organization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-10 18:36:57 +09:00

575 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:provider/provider.dart';
import '../providers/app_lock_provider.dart';
import '../providers/notification_provider.dart';
import '../providers/subscription_provider.dart';
import '../providers/navigation_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../services/notification_service.dart';
import '../screens/sms_scan_screen.dart';
import 'package:url_launcher/url_launcher.dart';
import '../providers/theme_provider.dart';
import '../theme/adaptive_theme.dart';
import '../widgets/glassmorphic_scaffold.dart';
import '../widgets/glassmorphic_app_bar.dart';
import '../widgets/glassmorphism_card.dart';
import '../widgets/app_navigator.dart';
import '../theme/app_colors.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
// 알림 시점 라디오 버튼 생성 헬퍼 메서드
Widget _buildReminderDayRadio(BuildContext context,
NotificationProvider provider, int value, String label) {
final isSelected = provider.reminderDays == value;
return Expanded(
child: InkWell(
onTap: () => provider.setReminderDays(value),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
size: 24,
),
const SizedBox(height: 6),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
);
}
Future<void> _backupData(BuildContext context) async {
try {
final provider = context.read<SubscriptionProvider>();
final subscriptions = provider.subscriptions;
// 임시 디렉토리에 백업 파일 생성
final tempDir = await getTemporaryDirectory();
final backupFile =
File(path.join(tempDir.path, 'submanager_backup.json'));
// 구독 데이터를 JSON 형식으로 저장
final jsonData = subscriptions
.map((sub) => {
'id': sub.id,
'serviceName': sub.serviceName,
'monthlyCost': sub.monthlyCost,
'billingCycle': sub.billingCycle,
'nextBillingDate': sub.nextBillingDate.toIso8601String(),
'isAutoDetected': sub.isAutoDetected,
'repeatCount': sub.repeatCount,
'lastPaymentDate': sub.lastPaymentDate?.toIso8601String(),
})
.toList();
await backupFile.writeAsString(jsonData.toString());
// 파일 공유
await Share.shareXFiles(
[XFile(backupFile.path)],
text: 'SubManager 백업 파일',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('백업 파일이 생성되었습니다')),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('백업 중 오류가 발생했습니다: $e')),
);
}
}
}
// SMS 스캔 화면으로 이동
void _navigateToSmsScan(BuildContext context) async {
final added = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (context) => const SmsScanScreen()),
);
if (added == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('구독이 성공적으로 추가되었습니다')),
);
}
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.only(top: 20),
children: [
// 테마 설정
GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'테마 설정',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
// 테마 모드 선택
ListTile(
title: const Text('테마 모드'),
subtitle: Text(_getThemeModeText(themeProvider.themeMode)),
leading: Icon(
_getThemeModeIcon(themeProvider.themeMode),
color: Theme.of(context).colorScheme.primary,
),
trailing: DropdownButton<AppThemeMode>(
value: themeProvider.themeMode,
underline: Container(),
onChanged: (mode) {
if (mode != null) {
themeProvider.setThemeMode(mode);
}
},
items: AppThemeMode.values.map((mode) =>
DropdownMenuItem(
value: mode,
child: Text(_getThemeModeText(mode)),
),
).toList(),
),
),
const Divider(height: 1),
// 접근성 설정
SwitchListTile(
title: const Text('큰 텍스트'),
subtitle: const Text('텍스트 크기를 크게 표시합니다'),
secondary: const Icon(Icons.text_fields),
value: themeProvider.largeText,
onChanged: themeProvider.setLargeText,
),
SwitchListTile(
title: const Text('모션 감소'),
subtitle: const Text('애니메이션 효과를 줄입니다'),
secondary: const Icon(Icons.slow_motion_video),
value: themeProvider.reduceMotion,
onChanged: themeProvider.setReduceMotion,
),
SwitchListTile(
title: const Text('고대비 모드'),
subtitle: const Text('더 선명한 색상으로 표시합니다'),
secondary: const Icon(Icons.contrast),
value: themeProvider.highContrast,
onChanged: themeProvider.setHighContrast,
),
],
);
},
),
),
// 앱 잠금 설정 UI 숨김
// Card(
// margin: const EdgeInsets.all(16),
// child: Consumer<AppLockProvider>(
// builder: (context, provider, child) {
// return SwitchListTile(
// title: const Text('앱 잠금'),
// subtitle: const Text('생체 인증으로 앱 잠금'),
// value: provider.isEnabled,
// onChanged: (value) async {
// if (value) {
// final isAuthenticated = await provider.authenticate();
// if (isAuthenticated) {
// provider.enable();
// }
// } else {
// provider.disable();
// }
// },
// );
// },
// ),
// ),
// 알림 설정
GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: Consumer<NotificationProvider>(
builder: (context, provider, child) {
return Column(
children: [
ListTile(
title: const Text('알림 권한'),
subtitle: const Text('알림을 받으려면 권한이 필요합니다'),
trailing: ElevatedButton(
onPressed: () async {
final granted =
await NotificationService.requestPermission();
if (granted) {
provider.setEnabled(true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('알림 권한이 거부되었습니다'),
),
);
}
},
child: const Text('권한 요청'),
),
),
const Divider(),
// 결제 예정 알림 기본 스위치
SwitchListTile(
title: const Text('결제 예정 알림'),
subtitle: const Text('결제 예정일 알림 받기'),
value: provider.isPaymentEnabled,
onChanged: (value) {
provider.setPaymentEnabled(value);
},
),
// 알림 세부 설정 (알림 활성화된 경우에만 표시)
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: provider.isPaymentEnabled
? Padding(
padding: const EdgeInsets.only(
left: 16.0, right: 16.0, bottom: 8.0),
child: Card(
elevation: 0,
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
// 알림 시점 선택 (1일전, 2일전, 3일전)
const Text('알림 시점',
style: TextStyle(
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
_buildReminderDayRadio(
context, provider, 1, '1일 전'),
_buildReminderDayRadio(
context, provider, 2, '2일 전'),
_buildReminderDayRadio(
context, provider, 3, '3일 전'),
],
),
),
const SizedBox(height: 16),
// 알림 시간 선택
const Text('알림 시간',
style: TextStyle(
fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
InkWell(
onTap: () async {
final TimeOfDay? picked =
await showTimePicker(
context: context,
initialTime: TimeOfDay(
hour: provider.reminderHour,
minute:
provider.reminderMinute),
);
if (picked != null) {
provider.setReminderTime(
picked.hour, picked.minute);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
borderRadius:
BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: Row(
children: [
Icon(
Icons.access_time,
color: Theme.of(context)
.colorScheme
.primary,
size: 22,
),
const SizedBox(width: 12),
Text(
'${provider.reminderHour.toString().padLeft(2, '0')}:${provider.reminderMinute.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 16,
fontWeight:
FontWeight.bold,
color: Theme.of(context)
.colorScheme
.onSurface,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context)
.colorScheme
.outline,
),
],
),
),
),
// 반복 알림 스위치 (2일전, 3일전 선택 시에만 활성화)
if (provider.reminderDays >= 2)
Padding(
padding:
const EdgeInsets.only(top: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 4),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withValues(alpha: 0.3),
borderRadius:
BorderRadius.circular(8),
),
child: SwitchListTile(
contentPadding:
const EdgeInsets.symmetric(
horizontal: 12),
title: const Text('1일마다 반복 알림'),
subtitle: const Text(
'결제일까지 매일 알림을 받습니다'),
value: provider
.isDailyReminderEnabled,
activeColor: Theme.of(context)
.colorScheme
.primary,
onChanged: (value) {
provider
.setDailyReminderEnabled(
value);
},
),
),
),
],
),
),
),
)
: const SizedBox.shrink(),
),
// 미사용 서비스 알림 기능 비활성화
// const Divider(),
// SwitchListTile(
// title: const Text('미사용 서비스 알림'),
// subtitle: const Text('2개월 이상 미사용 시 알림'),
// value: provider.isUnusedServiceNotificationEnabled,
// onChanged: (value) {
// provider.setUnusedServiceNotificationEnabled(value);
// },
// ),
],
);
},
),
),
// 데이터 관리
GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: Column(
children: [
// 데이터 백업 기능 비활성화
// ListTile(
// title: const Text('데이터 백업'),
// subtitle: const Text('구독 데이터를 백업합니다'),
// leading: const Icon(Icons.backup),
// onTap: () => _backupData(context),
// ),
// const Divider(),
],
),
),
// 앱 정보
GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: ListTile(
title: const Text('앱 정보'),
subtitle: const Text('버전 1.0.0'),
leading: const Icon(Icons.info),
onTap: () async {
// 웹 환경에서는 기본 다이얼로그 표시
if (kIsWeb) {
showAboutDialog(
context: context,
applicationName: 'SubManager',
applicationVersion: '1.0.0',
applicationIcon: const FlutterLogo(size: 50),
children: [
const Text('구독 관리 앱'),
const SizedBox(height: 8),
const Text('개발자: SubManager Team'),
],
);
return;
}
// 앱 스토어 링크
String storeUrl = '';
// 플랫폼에 따라 스토어 링크 설정
if (Platform.isAndroid) {
// Android - Google Play 스토어 링크
storeUrl =
'https://play.google.com/store/apps/details?id=com.submanager.app';
} else if (Platform.isIOS) {
// iOS - App Store 링크
storeUrl =
'https://apps.apple.com/app/submanager/id123456789';
}
if (storeUrl.isNotEmpty) {
try {
final Uri url = Uri.parse(storeUrl);
await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('스토어를 열 수 없습니다')),
);
}
}
} else {
// 스토어 링크를 열 수 없는 경우 기존 정보 다이얼로그 표시
showAboutDialog(
context: context,
applicationName: 'SubManager',
applicationVersion: '1.0.0',
applicationIcon: const FlutterLogo(size: 50),
children: [
const Text('구독 관리 앱'),
const SizedBox(height: 8),
const Text('개발자: SubManager Team'),
],
);
}
},
),
),
SizedBox(
height: 20 + MediaQuery.of(context).padding.bottom, // 하단 여백
),
],
);
}
String _getThemeModeText(AppThemeMode mode) {
switch (mode) {
case AppThemeMode.light:
return '라이트';
case AppThemeMode.dark:
return '다크';
case AppThemeMode.oled:
return 'OLED 블랙';
case AppThemeMode.system:
return '시스템 설정';
}
}
IconData _getThemeModeIcon(AppThemeMode mode) {
switch (mode) {
case AppThemeMode.light:
return Icons.light_mode;
case AppThemeMode.dark:
return Icons.dark_mode;
case AppThemeMode.oled:
return Icons.phonelink_lock;
case AppThemeMode.system:
return Icons.settings_brightness;
}
}
}