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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user