백품타에 스페이스바 단축키 추가 - 시작/일시정지/재개 토글

멈춰있으면 시작, 돌아가는 중이면 일시정지, 일시정지 중이면 재개로
스페이스바 한 키로 토글되게 했다. 뽀모도로가 공부/휴식을 자동으로
전환하는 중에는 화면에서도 수동 버튼을 숨기므로 스페이스바도 똑같이
막아서 자동 전환을 방해하지 않게 한다. 뽀모도로 설정 모달을 닫으면
포커스를 다시 돌려줘서 스페이스바가 계속 먹히게 처리.
This commit is contained in:
2026-10-01 04:08:27 +00:00
parent a29efe77ad
commit 13a86ec95e
+115 -76
View File
@@ -2,6 +2,7 @@
// 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다. // 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다.
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../function/browser_notification.dart'; import '../function/browser_notification.dart';
import '../function/study_timer_controller.dart'; import '../function/study_timer_controller.dart';
import '../theme/app_palette.dart'; import '../theme/app_palette.dart';
@@ -31,6 +32,9 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
with WidgetsBindingObserver { with WidgetsBindingObserver {
late final StudyTimerController _controller; late final StudyTimerController _controller;
Timer? _awayTimer; Timer? _awayTimer;
// ⌨️ 스페이스바로 시작/일시정지를 토글하기 위한 포커스 노드. 모달(뽀모도로 설정 등)이
// 닫히면 포커스를 다시 여기로 돌려줘야 스페이스바가 계속 먹힌다.
final FocusNode _keyboardFocusNode = FocusNode();
@override @override
void initState() { void initState() {
@@ -89,9 +93,30 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_awayTimer?.cancel(); _awayTimer?.cancel();
_controller.dispose(); _controller.dispose();
_keyboardFocusNode.dispose();
super.dispose(); super.dispose();
} }
/// ⌨️ 스페이스바 입력 처리 - 멈춰있으면 시작, 돌아가고 있으면 일시정지,
/// 일시정지 중이면 재개. 뽀모도로가 자동으로 공부↔휴식을 전환하는 중에는
/// 화면에서도 수동 일시정지/재개 버튼을 숨기므로 스페이스바도 똑같이 막는다.
void _handleSpacePress() {
if (_controller.isBusy) return;
if (_controller.isIdle) {
requestNotificationPermission();
_runAction(_controller.start);
return;
}
if (_controller.pomodoroEnabled && _controller.phase != null) {
return;
}
if (_controller.isRunning) {
_runAction(_controller.pause);
} else if (_controller.isPaused) {
_runAction(_controller.resume);
}
}
String _formatHMS(int totalSeconds) { String _formatHMS(int totalSeconds) {
final h = totalSeconds ~/ 3600; final h = totalSeconds ~/ 3600;
final m = (totalSeconds % 3600) ~/ 60; final m = (totalSeconds % 3600) ~/ 60;
@@ -275,6 +300,8 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
); );
}, },
); );
// 모달이 닫히면 스페이스바 토글이 다시 먹히도록 포커스를 돌려준다.
_keyboardFocusNode.requestFocus();
} }
Widget _minuteStepper({ Widget _minuteStepper({
@@ -336,91 +363,103 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
builder: (context, _) { builder: (context, _) {
final bool isRunning = _controller.isRunning; final bool isRunning = _controller.isRunning;
final bool isPaused = _controller.isPaused; final bool isPaused = _controller.isPaused;
return Scaffold( return Focus(
backgroundColor: AppPalette.mist, focusNode: _keyboardFocusNode,
appBar: AppBar( autofocus: true,
backgroundColor: Colors.transparent, onKeyEvent: (node, event) {
foregroundColor: AppPalette.ink, if (event is KeyDownEvent &&
elevation: 0, event.logicalKey == LogicalKeyboardKey.space) {
centerTitle: true, _handleSpacePress();
title: const TitlePill('백품타'), return KeyEventResult.handled;
actions: [ }
IconButton( return KeyEventResult.ignored;
icon: const Icon(Icons.tune_rounded), },
tooltip: '뽀모도로 설정', child: Scaffold(
onPressed: _showPomodoroSettings, backgroundColor: AppPalette.mist,
), appBar: AppBar(
IconButton( backgroundColor: Colors.transparent,
icon: const Icon(Icons.leaderboard_rounded), foregroundColor: AppPalette.ink,
tooltip: '학년 랭킹', elevation: 0,
onPressed: _openRanking, centerTitle: true,
), title: const TitlePill('백품타'),
], actions: [
), IconButton(
body: _controller.isLoading icon: const Icon(Icons.tune_rounded),
? const Center(child: CircularProgressIndicator()) tooltip: '뽀모도로 설정',
: Center( onPressed: _showPomodoroSettings,
child: SingleChildScrollView( ),
padding: const EdgeInsets.all(24), IconButton(
child: Column( icon: const Icon(Icons.leaderboard_rounded),
mainAxisSize: MainAxisSize.min, tooltip: '학년 랭킹',
children: [ onPressed: _openRanking,
Text( ),
_statusLabel(isRunning, isPaused), ],
style: const TextStyle( ),
fontSize: 15, body: _controller.isLoading
color: Colors.grey, ? const Center(child: CircularProgressIndicator())
), : Center(
), child: SingleChildScrollView(
if (_controller.pomodoroEnabled && padding: const EdgeInsets.all(24),
_controller.phase != null) ...[ child: Column(
const SizedBox(height: 4), mainAxisSize: MainAxisSize.min,
children: [
Text( Text(
'${_controller.phase == PomodoroPhase.study ? '다음 휴식까지' : '다음 공부까지'} ' _statusLabel(isRunning, isPaused),
'${_formatMinSec(_controller.phaseSecondsLeft)} · '
'완료 ${_controller.completedCycles}회',
style: const TextStyle( style: const TextStyle(
fontSize: 12.5, fontSize: 15,
color: Colors.grey, color: Colors.grey,
fontWeight: FontWeight.w600,
), ),
), ),
], if (_controller.pomodoroEnabled &&
const SizedBox(height: 12), _controller.phase != null) ...[
Text( const SizedBox(height: 4),
_formatHMS(_controller.liveElapsedSeconds), Text(
style: TextStyle( '${_controller.phase == PomodoroPhase.study ? '다음 휴식까지' : '다음 공부까지'} '
fontSize: 52, '${_formatMinSec(_controller.phaseSecondsLeft)} · '
fontWeight: FontWeight.bold, '완료 ${_controller.completedCycles}회',
color: isPaused ? Colors.grey : AppPalette.ink, style: const TextStyle(
), fontSize: 12.5,
), color: Colors.grey,
const SizedBox(height: 36), fontWeight: FontWeight.w600,
_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),
), ),
], ],
), 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),
),
],
),
],
),
), ),
), ),
), ),
); );
}, },
); );