// ⏱️ 우리 학교 전용 "백품타" 공부 타이머 화면 (UI 전용). // 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다. import 'package:flutter/material.dart'; import '../function/study_timer_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'launchpad_transition.dart'; import 'study_preset_screen.dart'; import 'study_ranking_screen.dart'; import 'title_pill.dart'; class StudyTimerScreen extends StatefulWidget { final String studentId; final String studentName; final int? grade; const StudyTimerScreen({ super.key, required this.studentId, required this.studentName, this.grade, }); @override State createState() => _StudyTimerScreenState(); } class _StudyTimerScreenState extends State { late final StudyTimerController _controller; @override void initState() { super.initState(); _controller = StudyTimerController( studentId: widget.studentId, studentName: widget.studentName, ); _controller.onPhaseChanged = _handlePhaseChanged; _controller.init(); } void _handlePhaseChanged(String message) { if (!mounted) return; AppNotice.show(context, message, icon: Icons.timer_rounded); } @override void dispose() { _controller.dispose(); super.dispose(); } String _formatHMS(int totalSeconds) { final h = totalSeconds ~/ 3600; final m = (totalSeconds % 3600) ~/ 60; final s = totalSeconds % 60; final hh = h.toString().padLeft(2, '0'); final mm = m.toString().padLeft(2, '0'); final ss = s.toString().padLeft(2, '0'); return '$hh:$mm:$ss'; } String _formatMinSec(int totalSeconds) { final s = totalSeconds < 0 ? 0 : totalSeconds; final m = s ~/ 60; final sec = s % 60; return '$m:${sec.toString().padLeft(2, '0')}'; } String _statusLabel(bool isRunning, bool isPaused) { if (_controller.pomodoroEnabled && _controller.phase != null) { return _controller.phase == PomodoroPhase.study ? '뽀모도로 - 공부 중이에요' : '뽀모도로 - 쉬는 중이에요'; } if (isRunning) return '지금 공부 중이에요'; if (isPaused) return '일시정지 중이에요'; return '공부를 시작해볼까요?'; } String _formatHM(int totalSeconds) { final h = totalSeconds ~/ 3600; final m = (totalSeconds % 3600) ~/ 60; if (h > 0) return '$h시간 $m분'; return '$m분'; } Future _runAction( Future<(bool success, String message)> Function() action, ) async { final (success, message) = await action(); if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); } Future _showPomodoroSettings() async { bool enabled = _controller.pomodoroEnabled; int studyMinutes = _controller.studyMinutes; int breakMinutes = _controller.breakMinutes; final bool canEdit = _controller.isIdle; await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: AppPalette.paper, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (sheetContext) { return StatefulBuilder( builder: (sheetContext, setSheetState) { return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ const Expanded( child: Text( '뽀모도로 모드', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ), Switch( value: enabled, activeThumbColor: AppPalette.ink, onChanged: canEdit ? (v) => setSheetState(() => enabled = v) : null, ), ], ), const Text( '공부/휴식 시간을 정해두면, 공부 시간이 끝났을 때 자동으로\n일시정지되고 휴식이 끝나면 자동으로 다시 시작해요.', style: TextStyle(color: Colors.grey, fontSize: 12.5), ), if (!canEdit) ...[ const SizedBox(height: 10), const Text( '타이머가 멈춰있을 때만 설정을 바꿀 수 있어요.', style: TextStyle(color: Colors.redAccent, fontSize: 12.5), ), ], const SizedBox(height: 18), _minuteStepper( label: '공부 시간', minutes: studyMinutes, enabled: canEdit, onChanged: (v) => setSheetState(() => studyMinutes = v), ), const SizedBox(height: 12), _minuteStepper( label: '휴식 시간', minutes: breakMinutes, enabled: canEdit, onChanged: (v) => setSheetState(() => breakMinutes = v), ), const SizedBox(height: 10), OutlinedButton.icon( icon: const Icon(Icons.groups_rounded, size: 18), label: const Text('친구들이 공유한 루틴 보기 / 공유하기'), onPressed: () async { final result = await pushLaunchpad>( sheetContext, (context) => StudyPresetScreen( studentId: widget.studentId, studentName: widget.studentName, initialStudyMinutes: studyMinutes, initialBreakMinutes: breakMinutes, ), ); if (result == null) return; if (!canEdit) { if (sheetContext.mounted) { AppNotice.show( sheetContext, '타이머가 멈춰있을 때만 루틴을 적용할 수 있어요.', ); } return; } setSheetState(() { studyMinutes = result['studyMinutes']!; breakMinutes = result['breakMinutes']!; enabled = true; }); }, ), const SizedBox(height: 18), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: !canEdit ? null : () async { await _controller.setPomodoroSettings( enabled: enabled, studyMinutes: studyMinutes, breakMinutes: breakMinutes, ); if (sheetContext.mounted) { Navigator.of(sheetContext).pop(); } }, child: const Text('저장'), ), ], ), ); }, ); }, ); } Widget _minuteStepper({ required String label, required int minutes, required bool enabled, required ValueChanged onChanged, }) { return Row( children: [ Expanded( child: Text( label, style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon(Icons.remove_circle_outline_rounded), onPressed: enabled && minutes > 5 ? () => onChanged(minutes - 5) : null, ), SizedBox( width: 48, child: Text( '$minutes분', textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.add_circle_outline_rounded), onPressed: enabled && minutes < 120 ? () => onChanged(minutes + 5) : null, ), ], ); } void _openRanking() { if (widget.grade == null) { AppNotice.show(context, '학년 정보가 없어 랭킹을 볼 수 없어요.'); return; } pushLaunchpad( context, (context) => StudyRankingScreen( grade: widget.grade!, myStudentId: widget.studentId, ), ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { final bool isRunning = _controller.isRunning; final bool isPaused = _controller.isPaused; return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, centerTitle: true, title: const TitlePill('백품타'), actions: [ IconButton( icon: const Icon(Icons.tune_rounded), tooltip: '뽀모도로 설정', onPressed: _showPomodoroSettings, ), IconButton( icon: const Icon(Icons.leaderboard_rounded), tooltip: '학년 랭킹', onPressed: _openRanking, ), ], ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _statusLabel(isRunning, isPaused), style: const TextStyle( fontSize: 15, color: Colors.grey, ), ), if (_controller.pomodoroEnabled && _controller.phase != null) ...[ const SizedBox(height: 4), Text( '${_controller.phase == PomodoroPhase.study ? '다음 휴식까지' : '다음 공부까지'} ' '${_formatMinSec(_controller.phaseSecondsLeft)} · ' '완료 ${_controller.completedCycles}회', style: const TextStyle( fontSize: 12.5, color: Colors.grey, fontWeight: FontWeight.w600, ), ), ], const SizedBox(height: 12), Text( _formatHMS(_controller.liveElapsedSeconds), style: TextStyle( fontSize: 52, fontWeight: FontWeight.bold, color: isPaused ? Colors.grey : AppPalette.ink, ), ), const SizedBox(height: 36), _buildActionButtons(isRunning, isPaused), const SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _statBox( '오늘', _formatHM(_controller.todaySecondsLive), ), const SizedBox(width: 12), _statBox( '이번주', _formatHM(_controller.weekSecondsLive), ), const SizedBox(width: 12), _statBox( '전체', _formatHM(_controller.totalSecondsLive), ), ], ), ], ), ), ), ); }, ); } Widget _buildActionButtons(bool isRunning, bool isPaused) { if (!isRunning && !isPaused) { return _circleButton( size: 140, icon: Icons.play_arrow_rounded, label: _controller.pomodoroEnabled ? '뽀모도로 시작' : '공부 시작', color: AppPalette.ink, onTap: () => _runAction(_controller.start), ); } if (_controller.pomodoroEnabled && _controller.phase != null) { // 뽀모도로 자동 전환 중에는 일시정지/재개는 자동으로 일어나니 종료 버튼만 보여준다. return _circleButton( size: 140, icon: Icons.stop_rounded, label: '공부 종료', color: Colors.red[400]!, onTap: () => _runAction(_controller.stop), ); } return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _circleButton( size: 110, icon: isPaused ? Icons.play_arrow_rounded : Icons.pause_rounded, label: isPaused ? '재개' : '일시정지', color: isPaused ? AppPalette.ink : Colors.amber[700]!, onTap: () => _runAction(isPaused ? _controller.resume : _controller.pause), ), const SizedBox(width: 20), _circleButton( size: 110, icon: Icons.stop_rounded, label: '공부 종료', color: Colors.red[400]!, onTap: () => _runAction(_controller.stop), ), ], ); } Widget _circleButton({ required double size, required IconData icon, required String label, required Color color, required VoidCallback onTap, }) { return GestureDetector( onTap: _controller.isBusy ? null : onTap, child: Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: color, boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Center( child: _controller.isBusy ? const CircularProgressIndicator(color: Colors.white) : Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: Colors.white, size: size > 120 ? 36 : 28), Text( label, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: size > 120 ? 14 : 12.5, ), ), ], ), ), ), ); } Widget _statBox(String label, String value) { return Container( width: 100, padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Column( children: [ Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 12)), const SizedBox(height: 4), Text( value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ], ), ); } }