i8n과 광고 수정
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../controllers/add_subscription_controller.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../common/form_fields/currency_input_field.dart';
|
||||
import '../common/form_fields/date_picker_field.dart';
|
||||
// import '../../theme/app_colors.dart';
|
||||
@@ -72,23 +73,9 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
const SizedBox(width: 12),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
String titleText;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
titleText = '이벤트 가격';
|
||||
break;
|
||||
case 'ja':
|
||||
titleText = 'イベント価格';
|
||||
break;
|
||||
case 'zh':
|
||||
titleText = '活动价格';
|
||||
break;
|
||||
default:
|
||||
titleText = 'Event Price';
|
||||
}
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Text(
|
||||
titleText,
|
||||
loc.eventPrice,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -157,23 +144,8 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final locale =
|
||||
Localizations.localeOf(context);
|
||||
String infoText;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
infoText = '할인 또는 프로모션 가격을 설정하세요';
|
||||
break;
|
||||
case 'ja':
|
||||
infoText = '割引またはプロモーション価格を設定してください';
|
||||
break;
|
||||
case 'zh':
|
||||
infoText = '设置折扣或促销价格';
|
||||
break;
|
||||
default:
|
||||
infoText =
|
||||
'Set up discount or promotion price';
|
||||
}
|
||||
final loc = AppLocalizations.of(context);
|
||||
final infoText = loc.eventPriceHint;
|
||||
return Text(
|
||||
infoText,
|
||||
style: TextStyle(
|
||||
@@ -195,26 +167,9 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
// 이벤트 기간
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final locale = Localizations.localeOf(context);
|
||||
String startLabel;
|
||||
String endLabel;
|
||||
switch (locale.languageCode) {
|
||||
case 'ko':
|
||||
startLabel = '시작일';
|
||||
endLabel = '종료일';
|
||||
break;
|
||||
case 'ja':
|
||||
startLabel = '開始日';
|
||||
endLabel = '終了日';
|
||||
break;
|
||||
case 'zh':
|
||||
startLabel = '开始日期';
|
||||
endLabel = '结束日期';
|
||||
break;
|
||||
default:
|
||||
startLabel = 'Start Date';
|
||||
endLabel = 'End Date';
|
||||
}
|
||||
final loc = AppLocalizations.of(context);
|
||||
final startLabel = loc.startDate;
|
||||
final endLabel = loc.endDate;
|
||||
return DateRangePickerField(
|
||||
startDate: controller.eventStartDate,
|
||||
endDate: controller.eventEndDate,
|
||||
@@ -245,37 +200,13 @@ class AddSubscriptionEventSection extends StatelessWidget {
|
||||
// 이벤트 가격
|
||||
Builder(
|
||||
builder: (BuildContext innerContext) {
|
||||
// 현재 로케일 확인
|
||||
final currentLocale =
|
||||
Localizations.localeOf(innerContext);
|
||||
|
||||
// 로케일에 따라 직접 텍스트 설정
|
||||
String eventPriceLabel;
|
||||
String eventPriceHint;
|
||||
|
||||
switch (currentLocale.languageCode) {
|
||||
case 'ko':
|
||||
eventPriceLabel = '이벤트 가격';
|
||||
eventPriceHint = '할인된 가격을 입력하세요';
|
||||
break;
|
||||
case 'ja':
|
||||
eventPriceLabel = 'イベント価格';
|
||||
eventPriceHint = '割引価格を入力してください';
|
||||
break;
|
||||
case 'zh':
|
||||
eventPriceLabel = '活动价格';
|
||||
eventPriceHint = '输入折扣价格';
|
||||
break;
|
||||
default:
|
||||
eventPriceLabel = 'Event Price';
|
||||
eventPriceHint = 'Enter discounted price';
|
||||
}
|
||||
final loc = AppLocalizations.of(innerContext);
|
||||
|
||||
return CurrencyInputField(
|
||||
controller: controller.eventPriceController,
|
||||
currency: controller.currency,
|
||||
label: eventPriceLabel,
|
||||
hintText: eventPriceHint,
|
||||
label: loc.eventPrice,
|
||||
hintText: loc.eventPriceHint,
|
||||
enabled: controller.isEventActive,
|
||||
// 이벤트 비활성화 시 검증을 건너뛰어 저장이 막히지 않도록 처리
|
||||
validator:
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../providers/subscription_provider.dart';
|
||||
import '../utils/subscription_grouping_helper.dart';
|
||||
import '../widgets/empty_state_widget.dart';
|
||||
import '../widgets/main_summary_card.dart';
|
||||
import '../widgets/native_ad_widget.dart';
|
||||
import '../theme/ui_constants.dart';
|
||||
import '../widgets/subscription_list_widget.dart';
|
||||
|
||||
class HomeContent extends StatefulWidget {
|
||||
@@ -115,13 +115,8 @@ class _HomeContentState extends State<HomeContent> {
|
||||
controller: widget.scrollController,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: NativeAdWidget(key: ValueKey('home_ad')),
|
||||
child: SizedBox(height: UIConstants.pageTopPadding),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SlideTransition(
|
||||
|
||||
@@ -11,7 +11,16 @@ import '../theme/ui_constants.dart';
|
||||
/// SRP에 따라 광고 전용 위젯으로 분리
|
||||
class NativeAdWidget extends StatefulWidget {
|
||||
final bool useOuterPadding; // true이면 외부에서 페이지 패딩을 제공
|
||||
const NativeAdWidget({super.key, this.useOuterPadding = false});
|
||||
final TemplateType? templateTypeOverride;
|
||||
final double? aspectRatioOverride;
|
||||
final MediaAspectRatio? mediaAspectRatioOverride;
|
||||
const NativeAdWidget({
|
||||
super.key,
|
||||
this.useOuterPadding = false,
|
||||
this.templateTypeOverride,
|
||||
this.aspectRatioOverride,
|
||||
this.mediaAspectRatioOverride,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NativeAdWidget> createState() => _NativeAdWidgetState();
|
||||
@@ -58,10 +67,14 @@ class _NativeAdWidgetState extends State<NativeAdWidget> {
|
||||
adUnitId: _testAdUnitId(), // 실제 광고 단위 ID
|
||||
// 네이티브 템플릿을 사용하면 NativeAdFactory 등록 없이도 동작합니다.
|
||||
nativeTemplateStyle: NativeTemplateStyle(
|
||||
templateType: TemplateType.small,
|
||||
templateType: widget.templateTypeOverride ?? TemplateType.medium,
|
||||
mainBackgroundColor: const Color(0x00000000),
|
||||
cornerRadius: 12,
|
||||
),
|
||||
nativeAdOptions: NativeAdOptions(
|
||||
mediaAspectRatio:
|
||||
widget.mediaAspectRatioOverride ?? MediaAspectRatio.square,
|
||||
),
|
||||
request: const AdRequest(),
|
||||
listener: NativeAdListener(
|
||||
onAdLoaded: (ad) {
|
||||
@@ -129,12 +142,19 @@ class _NativeAdWidgetState extends State<NativeAdWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _adSlotHeight(double availableWidth) {
|
||||
final safeWidth =
|
||||
availableWidth > 0 ? availableWidth : UIConstants.nativeAdWidth;
|
||||
final aspectRatio =
|
||||
widget.aspectRatioOverride ?? UIConstants.nativeAdAspectRatio;
|
||||
return safeWidth / aspectRatio;
|
||||
}
|
||||
|
||||
/// 웹용 광고 플레이스홀더 위젯
|
||||
Widget _buildWebPlaceholder() {
|
||||
Widget _buildWebPlaceholder(double slotHeight, double horizontalPadding) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.useOuterPadding ? 0 : UIConstants.pageHorizontalPadding,
|
||||
horizontal: horizontalPadding,
|
||||
vertical: UIConstants.adVerticalPadding,
|
||||
),
|
||||
child: Card(
|
||||
@@ -143,7 +163,7 @@ class _NativeAdWidgetState extends State<NativeAdWidget> {
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
child: Container(
|
||||
height: UIConstants.adCardHeight,
|
||||
height: slotHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -232,43 +252,54 @@ class _NativeAdWidgetState extends State<NativeAdWidget> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// 웹 환경인 경우 플레이스홀더 표시
|
||||
if (kIsWeb) {
|
||||
return _buildWebPlaceholder();
|
||||
}
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double horizontalPadding =
|
||||
widget.useOuterPadding ? 0.0 : UIConstants.pageHorizontalPadding;
|
||||
final availableWidth = (constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth
|
||||
: MediaQuery.of(context).size.width) -
|
||||
(horizontalPadding * 2);
|
||||
final double slotHeight = _adSlotHeight(availableWidth);
|
||||
|
||||
// Android/iOS가 아닌 경우 광고 위젯을 렌더링하지 않음
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// 웹 환경인 경우 플레이스홀더 표시
|
||||
if (kIsWeb) {
|
||||
return _buildWebPlaceholder(slotHeight, horizontalPadding);
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
// 실패 시에도 동일 높이의 플레이스홀더를 유지하여 레이아웃 점프 방지
|
||||
return _buildWebPlaceholder();
|
||||
}
|
||||
// Android/iOS가 아닌 경우 광고 위젯을 렌더링하지 않음
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!_isLoaded) {
|
||||
// 로딩 중에도 실제 광고와 동일한 높이의 스켈레톤을 유지
|
||||
return _buildWebPlaceholder();
|
||||
}
|
||||
if (_error != null) {
|
||||
// 실패 시에도 동일 높이의 플레이스홀더를 유지하여 레이아웃 점프 방지
|
||||
return _buildWebPlaceholder(slotHeight, horizontalPadding);
|
||||
}
|
||||
|
||||
// 광고 정상 노출
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.useOuterPadding ? 0 : UIConstants.pageHorizontalPadding,
|
||||
vertical: UIConstants.adVerticalPadding,
|
||||
),
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: UIConstants.adCardHeight,
|
||||
child: AdWidget(ad: _nativeAd!),
|
||||
),
|
||||
),
|
||||
if (!_isLoaded) {
|
||||
// 로딩 중에도 실제 광고와 동일한 높이의 스켈레톤을 유지
|
||||
return _buildWebPlaceholder(slotHeight, horizontalPadding);
|
||||
}
|
||||
|
||||
// 광고 정상 노출
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: UIConstants.adVerticalPadding,
|
||||
),
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: slotHeight,
|
||||
child: AdWidget(ad: _nativeAd!),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ class ScanInitialWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 광고 위젯 추가
|
||||
const NativeAdWidget(key: ValueKey('sms_scan_start_ad')),
|
||||
const SizedBox(height: 48),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
@@ -64,6 +61,8 @@ class ScanInitialWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const NativeAdWidget(key: ValueKey('sms_scan_start_ad')),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../widgets/native_ad_widget.dart';
|
||||
import '../../widgets/themed_text.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
@@ -8,33 +7,32 @@ class ScanLoadingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const NativeAdWidget(key: ValueKey('sms_scan_loading_ad')),
|
||||
const SizedBox(height: 48),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).scanningMessages,
|
||||
forceDark: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).findingSubscriptions,
|
||||
opacity: 0.7,
|
||||
forceDark: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
return SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).scanningMessages,
|
||||
forceDark: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ThemedText(
|
||||
AppLocalizations.of(context).findingSubscriptions,
|
||||
opacity: 0.7,
|
||||
forceDark: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import '../../widgets/common/buttons/secondary_button.dart';
|
||||
import '../../widgets/common/form_fields/base_text_field.dart';
|
||||
import '../../widgets/common/form_fields/category_selector.dart';
|
||||
import '../../widgets/common/snackbar/app_snackbar.dart';
|
||||
import '../../widgets/native_ad_widget.dart';
|
||||
import '../../widgets/payment_card/payment_card_selector.dart';
|
||||
import '../../services/currency_util.dart';
|
||||
import '../../utils/sms_scan/date_formatter.dart';
|
||||
@@ -87,9 +86,6 @@ class _SubscriptionCardWidgetState extends State<SubscriptionCardWidget> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 광고 위젯 추가
|
||||
const NativeAdWidget(key: ValueKey('sms_scan_result_ad')),
|
||||
const SizedBox(height: 16),
|
||||
if (_hasRawSmsMessage)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
|
||||
@@ -13,6 +13,8 @@ import './common/snackbar/app_snackbar.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../utils/logger.dart';
|
||||
import '../utils/subscription_grouping_helper.dart';
|
||||
import 'native_ad_widget.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
|
||||
/// 카테고리별로 구독 목록을 표시하는 위젯
|
||||
class SubscriptionListWidget extends StatelessWidget {
|
||||
@@ -28,134 +30,132 @@ class SubscriptionListWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sections = groups;
|
||||
int itemCounter = 0;
|
||||
final List<Widget> children = [];
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final group = sections[index];
|
||||
final subscriptions = group.subscriptions;
|
||||
for (final group in sections) {
|
||||
final subscriptions = group.subscriptions;
|
||||
final List<Widget> subscriptionItems = [];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SubscriptionGroupHeader(
|
||||
group: group,
|
||||
subscriptionCount: subscriptions.length,
|
||||
totalCostUSD: _calculateTotalByCurrency(subscriptions, 'USD'),
|
||||
totalCostKRW: _calculateTotalByCurrency(subscriptions, 'KRW'),
|
||||
totalCostJPY: _calculateTotalByCurrency(subscriptions, 'JPY'),
|
||||
totalCostCNY: _calculateTotalByCurrency(subscriptions, 'CNY'),
|
||||
),
|
||||
// 카테고리별 구독 목록
|
||||
FadeTransition(
|
||||
opacity: Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: fadeController, curve: Curves.easeIn)),
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
cacheExtent: 500,
|
||||
itemCount: subscriptions.length,
|
||||
itemBuilder: (context, subIndex) {
|
||||
// 각 구독의 지연값 계산 (순차적으로 나타나도록)
|
||||
final delay = 0.05 * subIndex;
|
||||
const animationBegin = 0.2;
|
||||
const animationEnd = 1.0;
|
||||
final intervalStart = delay;
|
||||
final intervalEnd = intervalStart + 0.4;
|
||||
for (var subIndex = 0; subIndex < subscriptions.length; subIndex++) {
|
||||
// 각 구독의 지연값 계산 (순차적으로 나타나도록)
|
||||
final delay = 0.05 * subIndex;
|
||||
const animationBegin = 0.2;
|
||||
const animationEnd = 1.0;
|
||||
final intervalStart = delay;
|
||||
final intervalEnd = intervalStart + 0.4;
|
||||
|
||||
// 간격 계산 (0.0~1.0 사이의 값으로 정규화)
|
||||
final intervalStartNormalized =
|
||||
intervalStart.clamp(0.0, 0.9);
|
||||
final intervalEndNormalized = intervalEnd.clamp(0.1, 1.0);
|
||||
// 간격 계산 (0.0~1.0 사이의 값으로 정규화)
|
||||
final intervalStartNormalized = intervalStart.clamp(0.0, 0.9);
|
||||
final intervalEndNormalized = intervalEnd.clamp(0.1, 1.0);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: Tween<double>(
|
||||
begin: animationBegin, end: animationEnd)
|
||||
.animate(CurvedAnimation(
|
||||
parent: fadeController,
|
||||
curve: Interval(intervalStartNormalized,
|
||||
intervalEndNormalized,
|
||||
curve: Curves.easeOut))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: StaggeredAnimationItem(
|
||||
index: subIndex,
|
||||
delay: const Duration(milliseconds: 50),
|
||||
child: RepaintBoundary(
|
||||
child: SwipeableSubscriptionCard(
|
||||
subscription: subscriptions[subIndex],
|
||||
keepAlive: true,
|
||||
onTap: () {
|
||||
Log.d(
|
||||
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
|
||||
AppNavigator.toDetail(
|
||||
context, subscriptions[subIndex]);
|
||||
},
|
||||
onDelete: () async {
|
||||
// 현재 로케일에 맞는 서비스명 가져오기
|
||||
final localeProvider =
|
||||
Provider.of<LocaleProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final locale =
|
||||
localeProvider.locale.languageCode;
|
||||
final displayName =
|
||||
await SubscriptionUrlMatcher
|
||||
.getServiceDisplayName(
|
||||
serviceName:
|
||||
subscriptions[subIndex].serviceName,
|
||||
locale: locale,
|
||||
);
|
||||
|
||||
// 삭제 확인 다이얼로그 표시
|
||||
if (!context.mounted) return;
|
||||
final shouldDelete =
|
||||
await DeleteConfirmationDialog.show(
|
||||
context: context,
|
||||
serviceName: displayName,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (shouldDelete) {
|
||||
// 사용자가 확인한 경우에만 삭제 진행
|
||||
final provider =
|
||||
Provider.of<SubscriptionProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
await provider.deleteSubscription(
|
||||
subscriptions[subIndex].id,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: AppLocalizations.of(context)
|
||||
.subscriptionDeleted(displayName),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
subscriptionItems.add(
|
||||
FadeTransition(
|
||||
opacity: Tween<double>(begin: animationBegin, end: animationEnd)
|
||||
.animate(CurvedAnimation(
|
||||
parent: fadeController,
|
||||
curve: Interval(
|
||||
intervalStartNormalized, intervalEndNormalized,
|
||||
curve: Curves.easeOut))),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 6.0),
|
||||
child: StaggeredAnimationItem(
|
||||
index: subIndex,
|
||||
delay: const Duration(milliseconds: 50),
|
||||
child: RepaintBoundary(
|
||||
child: SwipeableSubscriptionCard(
|
||||
subscription: subscriptions[subIndex],
|
||||
keepAlive: true,
|
||||
onTap: () {
|
||||
Log.d(
|
||||
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
|
||||
AppNavigator.toDetail(context, subscriptions[subIndex]);
|
||||
},
|
||||
onDelete: () async {
|
||||
// 현재 로케일에 맞는 서비스명 가져오기
|
||||
final localeProvider = Provider.of<LocaleProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final locale = localeProvider.locale.languageCode;
|
||||
final displayName =
|
||||
await SubscriptionUrlMatcher.getServiceDisplayName(
|
||||
serviceName: subscriptions[subIndex].serviceName,
|
||||
locale: locale,
|
||||
);
|
||||
|
||||
// 삭제 확인 다이얼로그 표시
|
||||
if (!context.mounted) return;
|
||||
final shouldDelete = await DeleteConfirmationDialog.show(
|
||||
context: context,
|
||||
serviceName: displayName,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (shouldDelete) {
|
||||
// 사용자가 확인한 경우에만 삭제 진행
|
||||
final provider = Provider.of<SubscriptionProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
await provider.deleteSubscription(
|
||||
subscriptions[subIndex].id,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
AppSnackBar.showError(
|
||||
context: context,
|
||||
message: AppLocalizations.of(context)
|
||||
.subscriptionDeleted(displayName),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
itemCounter++;
|
||||
if ((itemCounter - 1) % 10 == 0) {
|
||||
subscriptionItems.add(
|
||||
NativeAdWidget(
|
||||
key: ValueKey('home_list_ad_$itemCounter'),
|
||||
aspectRatioOverride: 320 / 80,
|
||||
mediaAspectRatioOverride: MediaAspectRatio.landscape,
|
||||
templateTypeOverride: TemplateType.small,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: sections.length,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SubscriptionGroupHeader(
|
||||
group: group,
|
||||
subscriptionCount: subscriptions.length,
|
||||
totalCostUSD: _calculateTotalByCurrency(subscriptions, 'USD'),
|
||||
totalCostKRW: _calculateTotalByCurrency(subscriptions, 'KRW'),
|
||||
totalCostJPY: _calculateTotalByCurrency(subscriptions, 'JPY'),
|
||||
totalCostCNY: _calculateTotalByCurrency(subscriptions, 'CNY'),
|
||||
),
|
||||
...subscriptionItems,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate(children),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user