Refactor screens to MVC architecture with modular widgets

- Extract business logic from screens into dedicated controllers
- Split large screen files into smaller, reusable widget components
- Add controllers for AddSubscriptionScreen and DetailScreen
- Create modular widgets for subscription and detail features
- Improve code organization and maintainability
- Remove duplicated code and improve reusability

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-11 00:21:18 +09:00
parent 4731288622
commit 83c5e3d64e
56 changed files with 9092 additions and 4579 deletions

View File

@@ -1,16 +1,11 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:octo_image/octo_image.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:html/parser.dart' as html_parser;
import 'package:html/dom.dart' as html_dom;
import '../theme/app_colors.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
@@ -57,7 +52,7 @@ class FaviconCache {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('favicon_$serviceKey');
} catch (e) {
print('파비콘 캐시 로드 오류: $e');
// 파비콘 캐시 로드 오류
return null;
}
}
@@ -68,7 +63,7 @@ class FaviconCache {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('favicon_$serviceKey', logoUrl);
} catch (e) {
print('파비콘 캐시 저장 오류: $e');
// 파비콘 캐시 저장 오류
}
}
@@ -80,7 +75,7 @@ class FaviconCache {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('favicon_$serviceKey');
} catch (e) {
print('파비콘 캐시 삭제 오류: $e');
// 파비콘 캐시 삭제 오류
}
}
@@ -90,7 +85,7 @@ class FaviconCache {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('local_favicon_$serviceKey');
} catch (e) {
print('로컬 파비콘 경로 로드 오류: $e');
// 로컬 파비콘 경로 로드 오류
return null;
}
}
@@ -102,39 +97,14 @@ class FaviconCache {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('local_favicon_$serviceKey', filePath);
} catch (e) {
print('로컬 파비콘 경로 저장 오류: $e');
// 로컬 파비콘 경로 저장 오류
}
}
}
// 구글 파비콘 API 서비스
class GoogleFaviconService {
// CORS 프록시 서버 목록
static final List<String> _corsProxies = [
'https://corsproxy.io/?',
'https://api.allorigins.win/raw?url=',
'https://cors-anywhere.herokuapp.com/',
];
// 현재 사용 중인 프록시 인덱스
static int _currentProxyIndex = 0;
// 프록시를 사용하여 URL 생성
static String _getProxiedUrl(String url) {
// 앱 환경에서는 프록시 없이 직접 URL 반환
if (!kIsWeb) {
return url;
}
// 웹 환경에서는 CORS 프록시 사용
final proxy = _corsProxies[_currentProxyIndex];
_currentProxyIndex =
(_currentProxyIndex + 1) % _corsProxies.length; // 다음 요청은 다른 프록시 사용
// URL 인코딩
final encodedUrl = Uri.encodeComponent(url);
return '$proxy$encodedUrl';
}
// 구글 파비콘 API URL 생성
static String getFaviconUrl(String domain, int size) {
@@ -167,7 +137,7 @@ class GoogleFaviconService {
static String getBase64PlaceholderIcon(String serviceName, Color color) {
// 간단한 SVG 생성 (서비스 이름의 첫 글자를 원 안에 표시)
final initial = serviceName.isNotEmpty ? serviceName[0].toUpperCase() : '?';
final colorHex = color.value.toRadixString(16).padLeft(8, '0').substring(2);
final colorHex = color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2);
// 공백 없이 SVG 생성 (공백이 있으면 Base64 디코딩 후 이미지 로드 시 문제 발생)
final svgContent =
@@ -207,15 +177,12 @@ class _WebsiteIconState extends State<WebsiteIcon>
bool _isLoading = true;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
// 각 인스턴스에 대한 고유 식별자 추가
final String _uniqueId = DateTime.now().millisecondsSinceEpoch.toString();
// 서비스와 URL 조합으로 캐시 키 생성
String get _serviceKey => '${widget.serviceName}_${widget.url ?? ''}';
// 이전에 사용한 서비스 키 (URL이 변경됐는지 확인용)
String? _previousServiceKey;
// 로드 시작된 시점
DateTime? _loadStartTime;
@override
void initState() {
@@ -231,15 +198,9 @@ class _WebsiteIconState extends State<WebsiteIcon>
CurvedAnimation(
parent: _animationController, curve: Curves.easeOutCubic));
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut));
// 초기 _previousServiceKey 설정
_previousServiceKey = _serviceKey;
// 로드 시작 시간 기록
_loadStartTime = DateTime.now();
// 최초 로딩
_loadFaviconWithCache();
}
@@ -263,7 +224,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
// 이미 로딩 중인지 확인
if (!FaviconCache.markAsLoading(_serviceKey)) {
await Future.delayed(Duration(milliseconds: 500));
await Future.delayed(const Duration(milliseconds: 500));
cachedLogo = FaviconCache.getFromMemory(_serviceKey);
if (cachedLogo != null) {
setState(() {
@@ -312,7 +273,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
// 2. 이미 로딩 중인지 확인
if (!FaviconCache.markAsLoading(_serviceKey)) {
await Future.delayed(Duration(milliseconds: 500));
await Future.delayed(const Duration(milliseconds: 500));
localPath = await FaviconCache.getLocalFaviconPath(_serviceKey);
if (localPath != null) {
final file = File(localPath);
@@ -344,12 +305,9 @@ class _WebsiteIconState extends State<WebsiteIcon>
// 서비스명이나 URL이 변경된 경우에만 다시 로드
final currentServiceKey = _serviceKey;
if (_previousServiceKey != currentServiceKey) {
print('서비스 키 변경 감지: $_previousServiceKey -> $currentServiceKey');
// 서비스 키 변경 감지: $_previousServiceKey -> $currentServiceKey
_previousServiceKey = currentServiceKey;
// 로드 시작 시간 기록
_loadStartTime = DateTime.now();
// 변경된 서비스 정보로 파비콘 로드
_loadFaviconWithCache();
}
@@ -472,7 +430,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
return;
}
} catch (e) {
print('DuckDuckGo 파비콘 API 요청 실패: $e');
// DuckDuckGo 파비콘 API 요청 실패
// 실패 시 백업 방법으로 진행
}
@@ -501,7 +459,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
FaviconCache.cancelLoading(_serviceKey);
} catch (e) {
print('웹용 파비콘 가져오기 오류: $e');
// 웹용 파비콘 가져오기 오류
if (mounted) {
setState(() {
_isLoading = false;
@@ -579,7 +537,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
FaviconCache.cancelLoading(_serviceKey);
} catch (e) {
print('앱용 파비콘 다운로드 오류: $e');
// 앱용 파비콘 다운로드 오류
if (mounted) {
setState(() {
_isLoading = false;
@@ -610,7 +568,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
boxShadow: widget.isHovered
? [
BoxShadow(
color: _getColorFromName().withAlpha(76), // 약 0.3 알파값
color: _getColorFromName().withValues(alpha: 0.3), // 약 0.3 알파값
blurRadius: 12,
spreadRadius: 0,
offset: const Offset(0, 4),
@@ -643,7 +601,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.primaryColor.withAlpha(179)),
AppColors.primaryColor.withValues(alpha: 0.7)),
),
),
),
@@ -684,7 +642,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.primaryColor.withAlpha(179)),
AppColors.primaryColor.withValues(alpha: 0.7)),
),
),
),
@@ -726,7 +684,7 @@ class _WebsiteIconState extends State<WebsiteIcon>
gradient: LinearGradient(
colors: [
color,
color.withAlpha(204), // 약 0.8 알파값
color.withValues(alpha: 0.8), // 약 0.8 알파값
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,