From 13a86ec95e935d7760a6e367c72f58bc93d756ee Mon Sep 17 00:00:00 2001 From: sihoo Date: Thu, 1 Oct 2026 04:08:27 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=92=88=ED=83=80=EC=97=90=20?= =?UTF-8?q?=EC=8A=A4=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=B0=94=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=95=ED=82=A4=20=EC=B6=94=EA=B0=80=20-=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91/=EC=9D=BC=EC=8B=9C=EC=A0=95=EC=A7=80/=EC=9E=AC?= =?UTF-8?q?=EA=B0=9C=20=ED=86=A0=EA=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 멈춰있으면 시작, 돌아가는 중이면 일시정지, 일시정지 중이면 재개로 스페이스바 한 키로 토글되게 했다. 뽀모도로가 공부/휴식을 자동으로 전환하는 중에는 화면에서도 수동 버튼을 숨기므로 스페이스바도 똑같이 막아서 자동 전환을 방해하지 않게 한다. 뽀모도로 설정 모달을 닫으면 포커스를 다시 돌려줘서 스페이스바가 계속 먹히게 처리. --- lib/ui/study_timer_screen.dart | 191 ++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 76 deletions(-) diff --git a/lib/ui/study_timer_screen.dart b/lib/ui/study_timer_screen.dart index 17672dc..4918f5e 100644 --- a/lib/ui/study_timer_screen.dart +++ b/lib/ui/study_timer_screen.dart @@ -2,6 +2,7 @@ // 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다. import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../function/browser_notification.dart'; import '../function/study_timer_controller.dart'; import '../theme/app_palette.dart'; @@ -31,6 +32,9 @@ class _StudyTimerScreenState extends State with WidgetsBindingObserver { late final StudyTimerController _controller; Timer? _awayTimer; + // ⌨️ 스페이스바로 시작/일시정지를 토글하기 위한 포커스 노드. 모달(뽀모도로 설정 등)이 + // 닫히면 포커스를 다시 여기로 돌려줘야 스페이스바가 계속 먹힌다. + final FocusNode _keyboardFocusNode = FocusNode(); @override void initState() { @@ -89,9 +93,30 @@ class _StudyTimerScreenState extends State WidgetsBinding.instance.removeObserver(this); _awayTimer?.cancel(); _controller.dispose(); + _keyboardFocusNode.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) { final h = totalSeconds ~/ 3600; final m = (totalSeconds % 3600) ~/ 60; @@ -275,6 +300,8 @@ class _StudyTimerScreenState extends State ); }, ); + // 모달이 닫히면 스페이스바 토글이 다시 먹히도록 포커스를 돌려준다. + _keyboardFocusNode.requestFocus(); } Widget _minuteStepper({ @@ -336,91 +363,103 @@ class _StudyTimerScreenState extends State 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), + return Focus( + focusNode: _keyboardFocusNode, + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.space) { + _handleSpacePress(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: 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( - '${_controller.phase == PomodoroPhase.study ? '다음 휴식까지' : '다음 공부까지'} ' - '${_formatMinSec(_controller.phaseSecondsLeft)} · ' - '완료 ${_controller.completedCycles}회', + _statusLabel(isRunning, isPaused), style: const TextStyle( - fontSize: 12.5, + fontSize: 15, 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), + 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), + ), + ], + ), + ], + ), ), ), - ), + ), ); }, );