Compare commits

...

3 Commits

Author SHA1 Message Date
JiWoong Sul
6c92a323c0 feat(ui): 승리 오버레이 스크롤 UX 개선
- 터치 시 스크롤 속도업 기능 추가 (5배속)
- 스크롤 완료 후 수동 스크롤 모드 지원
- 속도업 안내 텍스트 표시
2026-01-14 23:23:48 +09:00
JiWoong Sul
8efd3e875c feat(l10n): 엔딩 스크롤 속도업 안내 문자열 추가
- endingHoldToSpeedUp: 길게 누르면 빨리 스크롤
- 한/영/일/중 4개 언어 지원
2026-01-14 23:23:43 +09:00
JiWoong Sul
01e26bb5f5 fix(l10n): 카피라이트 텍스트 대소문자 수정
- naturebridgeai → NatureBridgeAi
2026-01-14 23:23:37 +09:00
11 changed files with 166 additions and 56 deletions

View File

@@ -1792,7 +1792,7 @@ String get warningDeleteSave {
String get copyrightText {
// 카피라이트 텍스트는 언어에 따라 변하지 않음
return '© 2025 naturebridgeai & cclabs all rights reserved';
return '© 2025 NatureBridgeAi & cclabs all rights reserved';
}
// ============================================================================

View File

@@ -307,5 +307,8 @@
"@endingSkip": { "description": "Skip button" },
"endingTapToSkip": "TAP TO SKIP",
"@endingTapToSkip": { "description": "Tap to skip hint" }
"@endingTapToSkip": { "description": "Tap to skip hint" },
"endingHoldToSpeedUp": "HOLD TO SPEED UP",
"@endingHoldToSpeedUp": { "description": "Hold to speed up scrolling hint" }
}

View File

@@ -92,5 +92,6 @@
"endingHallOfFameLine2": "殿堂に記録されます",
"endingHallOfFameButton": "殿堂入り",
"endingSkip": "スキップ",
"endingTapToSkip": "タップでスキップ"
"endingTapToSkip": "タップでスキップ",
"endingHoldToSpeedUp": "長押しで高速スクロール"
}

View File

@@ -92,5 +92,6 @@
"endingHallOfFameLine2": "명예의 전당에 기록됩니다",
"endingHallOfFameButton": "명예의 전당",
"endingSkip": "건너뛰기",
"endingTapToSkip": "탭하여 건너뛰기"
"endingTapToSkip": "탭하여 건너뛰기",
"endingHoldToSpeedUp": "길게 누르면 빨리 스크롤"
}

View File

@@ -646,6 +646,12 @@ abstract class L10n {
/// In en, this message translates to:
/// **'TAP TO SKIP'**
String get endingTapToSkip;
/// Hold to speed up scrolling hint
///
/// In en, this message translates to:
/// **'HOLD TO SPEED UP'**
String get endingHoldToSpeedUp;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {

View File

@@ -294,4 +294,7 @@ class L10nEn extends L10n {
@override
String get endingTapToSkip => 'TAP TO SKIP';
@override
String get endingHoldToSpeedUp => 'HOLD TO SPEED UP';
}

View File

@@ -294,4 +294,7 @@ class L10nJa extends L10n {
@override
String get endingTapToSkip => 'タップでスキップ';
@override
String get endingHoldToSpeedUp => '長押しで高速スクロール';
}

View File

@@ -294,4 +294,7 @@ class L10nKo extends L10n {
@override
String get endingTapToSkip => '탭하여 건너뛰기';
@override
String get endingHoldToSpeedUp => '길게 누르면 빨리 스크롤';
}

View File

@@ -294,4 +294,7 @@ class L10nZh extends L10n {
@override
String get endingTapToSkip => '点击跳过';
@override
String get endingHoldToSpeedUp => '长按加速滚动';
}

View File

@@ -92,5 +92,6 @@
"endingHallOfFameLine2": "将被铭记于荣誉殿堂",
"endingHallOfFameButton": "荣誉殿堂",
"endingSkip": "跳过",
"endingTapToSkip": "点击跳过"
"endingTapToSkip": "点击跳过",
"endingHoldToSpeedUp": "长按加速滚动"
}

View File

@@ -34,107 +34,184 @@ class VictoryOverlay extends StatefulWidget {
class _VictoryOverlayState extends State<VictoryOverlay>
with SingleTickerProviderStateMixin {
late AnimationController _scrollController;
late AnimationController _animationController;
late Animation<double> _scrollAnimation;
// 스크롤이 완료(최하단 도달) 되었는지 여부
bool _isScrollComplete = false;
// 터치 중인지 여부 (터치 시 속도업)
bool _isTouching = false;
// 스크롤 완료 후 수동 스크롤용 컨트롤러
late ScrollController _manualScrollController;
// 스크롤 지속 시간 (밀리초)
static const _scrollDurationMs = 25000; // 25초
static const _scrollDurationMs = 25000; // 25초 (기본 속도)
static const _fastScrollDurationMs = 5000; // 5초 (빠른 속도, 5배속)
@override
void initState() {
super.initState();
_scrollController = AnimationController(
_manualScrollController = ScrollController();
_animationController = AnimationController(
duration: const Duration(milliseconds: _scrollDurationMs),
vsync: this,
);
_scrollAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _scrollController, curve: Curves.linear));
_scrollAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
);
// 스크롤 완료 시 버튼 표시 (자동 종료하지 않음)
_scrollController.addStatusListener((status) {
// 스크롤 완료 시 수동 스크롤 모드로 전환
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
_isScrollComplete = true;
});
_onScrollComplete();
}
});
_scrollController.forward();
_animationController.forward();
}
@override
void dispose() {
_scrollController.dispose();
_animationController.dispose();
_manualScrollController.dispose();
super.dispose();
}
/// 탭 시 스크롤 최하단으로 즉시 이동
void _skipToEnd() {
_scrollController.stop();
_scrollController.value = 1.0;
/// 스크롤 완료 시 호출
void _onScrollComplete() {
setState(() {
_isScrollComplete = true;
});
// 수동 스크롤 시 맨 아래에서 시작
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_manualScrollController.hasClients) {
_manualScrollController.jumpTo(
_manualScrollController.position.maxScrollExtent,
);
}
});
}
/// 터치 시작 - 스크롤 속도업
void _onTouchStart() {
if (_isScrollComplete) return;
setState(() {
_isTouching = true;
});
// 현재 진행도 저장
final currentProgress = _animationController.value;
final remainingProgress = 1.0 - currentProgress;
// 남은 시간 계산 (빠른 속도 기준)
final remainingDuration = Duration(
milliseconds: (remainingProgress * _fastScrollDurationMs).toInt(),
);
_animationController.duration = remainingDuration;
_animationController.forward(from: currentProgress);
}
/// 터치 종료 - 스크롤 속도 복원
void _onTouchEnd() {
if (_isScrollComplete) return;
setState(() {
_isTouching = false;
});
// 현재 진행도 저장
final currentProgress = _animationController.value;
final remainingProgress = 1.0 - currentProgress;
// 남은 시간 계산 (기본 속도 기준)
final remainingDuration = Duration(
milliseconds: (remainingProgress * _scrollDurationMs).toInt(),
);
_animationController.duration = remainingDuration;
_animationController.forward(from: currentProgress);
}
@override
Widget build(BuildContext context) {
final gold = RetroColors.goldOf(context);
final l10n = L10n.of(context);
return GestureDetector(
onTap: _isScrollComplete ? null : _skipToEnd, // 스크롤 중에만 탭으로 스킵
// 터치 시 스크롤 속도업 (스크롤 중에만)
onTapDown: _isScrollComplete ? null : (_) => _onTouchStart(),
onTapUp: _isScrollComplete ? null : (_) => _onTouchEnd(),
onTapCancel: _isScrollComplete ? null : _onTouchEnd,
onLongPressStart: _isScrollComplete ? null : (_) => _onTouchStart(),
onLongPressEnd: _isScrollComplete ? null : (_) => _onTouchEnd(),
onLongPressCancel: _isScrollComplete ? null : _onTouchEnd,
child: Material(
color: Colors.black,
child: SafeArea(
child: Stack(
children: [
// 스크롤되는 크레딧
AnimatedBuilder(
animation: _scrollAnimation,
builder: (context, child) {
return _buildScrollingCredits(context);
},
),
// 스킵 버튼 (스크롤 중에만 표시)
// 스크롤 중: 애니메이션 기반 스크롤
// 스크롤 완료: 수동 스크롤 가능
if (!_isScrollComplete)
Positioned(
top: 16,
right: 16,
child: TextButton(
onPressed: _skipToEnd,
child: Text(
L10n.of(context).endingSkip,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: gold.withValues(alpha: 0.5),
),
),
),
),
AnimatedBuilder(
animation: _scrollAnimation,
builder: (context, child) {
return _buildScrollingCredits(context);
},
)
else
_buildManualScrollableCredits(context),
// 하단 힌트 (스크롤 중에만 표시)
// 하단 터치 힌트 (스크롤 중에만 표시)
if (!_isScrollComplete)
Positioned(
bottom: 16,
left: 0,
right: 0,
child: Text(
L10n.of(context).endingTapToSkip,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: Colors.white.withValues(alpha: 0.2),
child: AnimatedOpacity(
opacity: _isTouching ? 0.0 : 1.0,
duration: const Duration(milliseconds: 200),
child: Text(
l10n.endingHoldToSpeedUp,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: Colors.white.withValues(alpha: 0.3),
),
textAlign: TextAlign.center,
),
),
),
// 속도업 표시 (터치 중에만 표시)
if (!_isScrollComplete && _isTouching)
Positioned(
top: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: gold.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'▶▶ x5',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: gold,
),
),
textAlign: TextAlign.center,
),
),
],
@@ -162,6 +239,15 @@ class _VictoryOverlayState extends State<VictoryOverlay>
);
}
/// 수동 스크롤 가능한 크레딧 (스크롤 완료 후)
Widget _buildManualScrollableCredits(BuildContext context) {
return SingleChildScrollView(
controller: _manualScrollController,
physics: const BouncingScrollPhysics(),
child: _buildCreditContent(context),
);
}
double _estimateContentHeight() {
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
// 명예의 전당 버튼 추가로 인해 높이 증가