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 { 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" }, "@endingSkip": { "description": "Skip button" },
"endingTapToSkip": "TAP TO SKIP", "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": "殿堂に記録されます", "endingHallOfFameLine2": "殿堂に記録されます",
"endingHallOfFameButton": "殿堂入り", "endingHallOfFameButton": "殿堂入り",
"endingSkip": "スキップ", "endingSkip": "スキップ",
"endingTapToSkip": "タップでスキップ" "endingTapToSkip": "タップでスキップ",
"endingHoldToSpeedUp": "長押しで高速スクロール"
} }

View File

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

View File

@@ -646,6 +646,12 @@ abstract class L10n {
/// In en, this message translates to: /// In en, this message translates to:
/// **'TAP TO SKIP'** /// **'TAP TO SKIP'**
String get endingTapToSkip; 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> { class _L10nDelegate extends LocalizationsDelegate<L10n> {

View File

@@ -294,4 +294,7 @@ class L10nEn extends L10n {
@override @override
String get endingTapToSkip => 'TAP TO SKIP'; 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 @override
String get endingTapToSkip => 'タップでスキップ'; String get endingTapToSkip => 'タップでスキップ';
@override
String get endingHoldToSpeedUp => '長押しで高速スクロール';
} }

View File

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

View File

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

View File

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

View File

@@ -34,109 +34,186 @@ class VictoryOverlay extends StatefulWidget {
class _VictoryOverlayState extends State<VictoryOverlay> class _VictoryOverlayState extends State<VictoryOverlay>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController _scrollController; late AnimationController _animationController;
late Animation<double> _scrollAnimation; late Animation<double> _scrollAnimation;
// 스크롤이 완료(최하단 도달) 되었는지 여부 // 스크롤이 완료(최하단 도달) 되었는지 여부
bool _isScrollComplete = false; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollController = AnimationController( _manualScrollController = ScrollController();
_animationController = AnimationController(
duration: const Duration(milliseconds: _scrollDurationMs), duration: const Duration(milliseconds: _scrollDurationMs),
vsync: this, vsync: this,
); );
_scrollAnimation = Tween<double>( _scrollAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
begin: 0.0, CurvedAnimation(parent: _animationController, curve: Curves.linear),
end: 1.0, );
).animate(CurvedAnimation(parent: _scrollController, curve: Curves.linear));
// 스크롤 완료 시 버튼 표시 (자동 종료하지 않음) // 스크롤 완료 시 수동 스크롤 모드로 전환
_scrollController.addStatusListener((status) { _animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
setState(() { _onScrollComplete();
_isScrollComplete = true;
});
} }
}); });
_scrollController.forward(); _animationController.forward();
} }
@override @override
void dispose() { void dispose() {
_scrollController.dispose(); _animationController.dispose();
_manualScrollController.dispose();
super.dispose(); super.dispose();
} }
/// 탭 시 스크롤 최하단으로 즉시 이동 /// 스크롤 완료 시 호출
void _skipToEnd() { void _onScrollComplete() {
_scrollController.stop();
_scrollController.value = 1.0;
setState(() { setState(() {
_isScrollComplete = true; _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final gold = RetroColors.goldOf(context); final gold = RetroColors.goldOf(context);
final l10n = L10n.of(context);
return GestureDetector( 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( child: Material(
color: Colors.black, color: Colors.black,
child: SafeArea( child: SafeArea(
child: Stack( child: Stack(
children: [ children: [
// 스크롤되는 크레딧 // 스크롤 중: 애니메이션 기반 스크롤
// 스크롤 완료: 수동 스크롤 가능
if (!_isScrollComplete)
AnimatedBuilder( AnimatedBuilder(
animation: _scrollAnimation, animation: _scrollAnimation,
builder: (context, child) { builder: (context, child) {
return _buildScrollingCredits(context); return _buildScrollingCredits(context);
}, },
), )
else
_buildManualScrollableCredits(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),
),
),
),
),
// 하단 탭 힌트 (스크롤 중에만 표시)
if (!_isScrollComplete) if (!_isScrollComplete)
Positioned( Positioned(
bottom: 16, bottom: 16,
left: 0, left: 0,
right: 0, right: 0,
child: AnimatedOpacity(
opacity: _isTouching ? 0.0 : 1.0,
duration: const Duration(milliseconds: 200),
child: Text( child: Text(
L10n.of(context).endingTapToSkip, l10n.endingHoldToSpeedUp,
style: TextStyle( style: TextStyle(
fontFamily: 'PressStart2P', fontFamily: 'PressStart2P',
fontSize: 8, fontSize: 8,
color: Colors.white.withValues(alpha: 0.2), color: Colors.white.withValues(alpha: 0.3),
), ),
textAlign: TextAlign.center, 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,
),
),
),
),
], ],
), ),
), ),
@@ -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() { double _estimateContentHeight() {
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용) // 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
// 명예의 전당 버튼 추가로 인해 높이 증가 // 명예의 전당 버튼 추가로 인해 높이 증가