백품타 타이머 켜놓고 탭 이탈 시 브라우저 알림 추가
공부 타이머가 진행 중일 때 브라우저 탭이 백그라운드로 가면(다른 탭/창/앱으로 이동) 20초 뒤에도 계속 그 상태면 "다시 돌아와서 집중해볼까요?" 브라우저 알림을 띄운다. dart:html 대신 package:web + js_interop로 구현하고, 웹이 아닌 빌드에서는 조건부 임포트로 아무 동작 안 하는 스텁을 쓴다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
// 🌐 플랫폼에 따라 웹 전용 구현/무동작 구현 중 하나로 연결되는 진입점.
|
||||||
|
export 'browser_notification_stub.dart'
|
||||||
|
if (dart.library.html) 'browser_notification_web.dart';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 🌐 웹이 아닌 빌드(Android 등)에서는 브라우저 알림 API 자체가 없으니 아무 동작도 안 한다.
|
||||||
|
Future<bool> requestNotificationPermission() async => false;
|
||||||
|
|
||||||
|
void showBrowserNotification(String title, String body) {}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 🌐 웹 전용 - 브라우저 알림(Notification) API로 "타이머 켜놓고 탭을 벗어났을 때" 같은
|
||||||
|
// 즉각적인 로컬 알림을 띄운다. FCM 푸시와 달리 서버 왕복 없이 바로 뜬다.
|
||||||
|
// 알림 API를 지원하지 않는 브라우저에서는 조용히 무시한다.
|
||||||
|
import 'dart:js_interop';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
Future<bool> requestNotificationPermission() async {
|
||||||
|
try {
|
||||||
|
if (web.Notification.permission == 'granted') return true;
|
||||||
|
final permission = await web.Notification.requestPermission().toDart;
|
||||||
|
return permission.toDart == 'granted';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showBrowserNotification(String title, String body) {
|
||||||
|
try {
|
||||||
|
if (web.Notification.permission != 'granted') return;
|
||||||
|
web.Notification(title, web.NotificationOptions(body: body));
|
||||||
|
} catch (_) {
|
||||||
|
// 조용히 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// ⏱️ 우리 학교 전용 "백품타" 공부 타이머 화면 (UI 전용).
|
// ⏱️ 우리 학교 전용 "백품타" 공부 타이머 화면 (UI 전용).
|
||||||
// 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다.
|
// 서버 통신/상태는 lib/function/study_timer_controller.dart가 담당한다.
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
import 'app_notice.dart';
|
import 'app_notice.dart';
|
||||||
@@ -28,6 +30,7 @@ class StudyTimerScreen extends StatefulWidget {
|
|||||||
class _StudyTimerScreenState extends State<StudyTimerScreen>
|
class _StudyTimerScreenState extends State<StudyTimerScreen>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
late final StudyTimerController _controller;
|
late final StudyTimerController _controller;
|
||||||
|
Timer? _awayTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -48,16 +51,34 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
// 🐛 [웹 탭 딜레이 버그 수정] 다른 창/탭에 갔다가 이 탭으로 돌아왔을 때(resumed) 밀린
|
|
||||||
// 시간을 바로 따라잡는다 - 백그라운드 탭에서는 브라우저가 타이머를 느리게 돌리기 때문.
|
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
// 🐛 [웹 탭 딜레이 버그 수정] 다른 창/탭에 갔다가 이 탭으로 돌아왔을 때(resumed) 밀린
|
||||||
|
// 시간을 바로 따라잡는다 - 백그라운드 탭에서는 브라우저가 타이머를 느리게 돌리기 때문.
|
||||||
|
_awayTimer?.cancel();
|
||||||
_controller.onAppResumed();
|
_controller.onAppResumed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 🔔 타이머가 켜져 있는데 탭이 백그라운드로 가면(다른 탭/창/앱으로 이동), 20초 뒤에도
|
||||||
|
// 계속 딴 데 가있으면 브라우저 알림으로 다시 공부하라고 알려준다.
|
||||||
|
final bool isAway =
|
||||||
|
state == AppLifecycleState.hidden ||
|
||||||
|
state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.paused;
|
||||||
|
if (isAway && _controller.isRunning) {
|
||||||
|
_awayTimer?.cancel();
|
||||||
|
_awayTimer = Timer(const Duration(seconds: 20), () {
|
||||||
|
showBrowserNotification(
|
||||||
|
'백품타 - 공부 중이었잖아요!',
|
||||||
|
'타이머가 켜져 있어요. 다시 돌아와서 집중해볼까요?',
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_awayTimer?.cancel();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -398,7 +419,10 @@ class _StudyTimerScreenState extends State<StudyTimerScreen>
|
|||||||
icon: Icons.play_arrow_rounded,
|
icon: Icons.play_arrow_rounded,
|
||||||
label: _controller.pomodoroEnabled ? '뽀모도로 시작' : '공부 시작',
|
label: _controller.pomodoroEnabled ? '뽀모도로 시작' : '공부 시작',
|
||||||
color: AppPalette.ink,
|
color: AppPalette.ink,
|
||||||
onTap: () => _runAction(_controller.start),
|
onTap: () {
|
||||||
|
requestNotificationPermission();
|
||||||
|
_runAction(_controller.start);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (_controller.pomodoroEnabled && _controller.phase != null) {
|
if (_controller.pomodoroEnabled && _controller.phase != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user