백품타에 뽀모도로 커스텀 모드 추가
공부/휴식 시간을 5분 단위로 직접 정해두면(기본 50/10), 공부 시간이 끝났을 때 자동으로 일시정지되고 휴식이 끝나면 자동으로 재개된다. 서버의 기존 일시정지/재개 API를 그대로 재사용하는 클라이언트 전용 스케줄러라 백엔드 변경은 없음. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,15 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
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();
|
||||
@@ -53,6 +59,24 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
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;
|
||||
@@ -72,6 +96,146 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPomodoroSettings() async {
|
||||
bool enabled = _controller.pomodoroEnabled;
|
||||
int studyMinutes = _controller.studyMinutes;
|
||||
int breakMinutes = _controller.breakMinutes;
|
||||
final bool canEdit = _controller.isIdle;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
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: 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<int> 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, '학년 정보가 없어 랭킹을 볼 수 없어요.');
|
||||
@@ -102,6 +266,11 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
centerTitle: true,
|
||||
title: const TitlePill('백품타'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune_rounded),
|
||||
tooltip: '뽀모도로 설정',
|
||||
onPressed: _showPomodoroSettings,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.leaderboard_rounded),
|
||||
tooltip: '학년 랭킹',
|
||||
@@ -118,16 +287,26 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
isRunning
|
||||
? '지금 공부 중이에요'
|
||||
: isPaused
|
||||
? '일시정지 중이에요'
|
||||
: '공부를 시작해볼까요?',
|
||||
_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),
|
||||
@@ -173,11 +352,21 @@ class _StudyTimerScreenState extends State<StudyTimerScreen> {
|
||||
return _circleButton(
|
||||
size: 140,
|
||||
icon: Icons.play_arrow_rounded,
|
||||
label: '공부 시작',
|
||||
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: [
|
||||
|
||||
Reference in New Issue
Block a user