-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
221 lines (193 loc) · 7.11 KB
/
background.js
File metadata and controls
221 lines (193 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
importScripts('js/i18n.js');
// 설치/시작 시 초기화
chrome.runtime.onInstalled.addListener(() => {
updateBlockingRules(); // 설치 직후 규칙 동기화
});
chrome.runtime.onStartup.addListener(() => {
updateBlockingRules(); // 브라우저 시작 시 규칙 동기화
});
// --- 메시지 핸들러 ---
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'toggleFocusMode') {
// 팝업에서 상태 변경 요청이 오면 규칙 즉시 업데이트
// (저장은 popup.js에서 이미 했으므로 여기선 규칙만 갱신)
updateBlockingRules();
updateExtensionIcon(request.enabled);
} else if (request.action === 'startTimer') {
const minutes = parseFloat(request.minutes);
chrome.alarms.create('focusTimer', { delayInMinutes: minutes });
} else if (request.action === 'resetTimer') {
chrome.alarms.clear('focusTimer');
}
});
// --- 알람(타이머) 이벤트 ---
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'focusTimer') {
try {
// 1. 알림 표시 (가장 우선)
await self.i18n.init();
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon64.png',
title: 'MindLock',
message: self.i18n.getMessage('timerFinished'),
priority: 2,
requireInteraction: true,
});
// 2. 집중 기록 저장 (데이터 보존 우선)
const localData = await chrome.storage.local.get(['timerOriginalDuration', 'focusHistory']);
const history = localData.focusHistory || [];
const duration = localData.timerOriginalDuration || 0;
let shouldOvertime = true;
const { isOvertimeEnabled } = await chrome.storage.sync.get('isOvertimeEnabled');
shouldOvertime = isOvertimeEnabled !== false;
const historyEntry = {
id: Date.now(),
startTime: Date.now() - (duration * 60 * 1000),
endTime: Date.now(),
duration: duration,
originalDuration: duration, // 목표 시간 저장
type: 'timer',
status: shouldOvertime ? 'in-progress' : 'completed'
};
history.unshift(historyEntry);
if (history.length > 100) history.pop();
await chrome.storage.local.set({ focusHistory: history });
if (shouldOvertime) {
// 진행 중인 기록 ID 저장
await chrome.storage.local.set({
timerStatus: 'overtime',
currentFocusRecordId: historyEntry.id
});
} else {
await chrome.storage.local.remove(['timerTargetTime', 'timerStatus', 'currentFocusRecordId']);
chrome.storage.sync.set({ isFocusModeEnabled: false }, () => {
updateBlockingRules();
updateExtensionIcon(false);
});
}
// 3. 소리 재생
const { isSoundEnabled } = await chrome.storage.sync.get('isSoundEnabled');
if (isSoundEnabled !== false) {
await Promise.race([
playSound(),
new Promise((_, reject) => setTimeout(() => reject(new Error('Sound timeout')), 3000))
]);
}
} catch (err) {
// Error in onAlarm handler
}
}
});
// 소리 재생을 위한 Offscreen 문서 생성 및 메시지 전송
async function playSound() {
const offscreenUrl = chrome.runtime.getURL('offscreen/offscreen.html');
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl],
});
if (existingContexts.length === 0) {
await chrome.offscreen.createDocument({
url: 'offscreen/offscreen.html',
reasons: ['AUDIO_PLAYBACK'],
justification: 'Play notification sound when timer finishes',
});
}
chrome.runtime.sendMessage({ action: 'playTimerSound' });
}
// --- 설정 변경 감지 (옵션 페이지 등에서 수정 시) ---
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'sync') {
if (changes.blockedUrls || changes.isFocusModeEnabled || changes.isSubdomainBlockEnabled) {
updateBlockingRules();
}
}
});
// --- 차단 규칙 동기화 함수 ---
let updatePromise = Promise.resolve();
async function updateBlockingRules() {
updatePromise = updatePromise.then(async () => {
await performRuleUpdate();
}).catch(err => { });
}
async function performRuleUpdate() {
// 1. 메모리 변수가 아닌, 실제 저장소(Storage)에서 최신 데이터를 가져옵니다.
const data = await chrome.storage.sync.get([
'isFocusModeEnabled',
'blockedUrls',
'isSubdomainBlockEnabled'
]);
const isEnabled = data.isFocusModeEnabled || false;
const blockedUrls = data.blockedUrls || [];
const isSubdomainBlockEnabled = data.isSubdomainBlockEnabled !== false; // 기본값 true (undefined일 때 true)
updateExtensionIcon(isEnabled);
// 2. 현재 등록된 모든 규칙 가져오기 및 삭제 준비
const currentRules = await chrome.declarativeNetRequest.getDynamicRules();
const removeRuleIds = currentRules.map((rule) => rule.id);
// 3. 집중 모드가 꺼져있거나 목록이 비었으면 -> 규칙 삭제만 하고 종료
if (!isEnabled || blockedUrls.length === 0) {
if (removeRuleIds.length > 0) {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: removeRuleIds,
});
}
return;
}
// 4. 새 규칙 생성
const blockedPageUrl = chrome.runtime.getURL('blocked/blocked.html');
const addRules = blockedUrls.map((url, index) => {
// 도메인 정제 logic
let domain = url.trim();
try {
const urlObj = new URL(
domain.startsWith('http') ? domain : `http://${domain}`
);
domain = urlObj.hostname;
} catch (e) {
domain = domain.replace(/^(https?:\/\/)?(www\.)?/, '').replace(/\/$/, '');
}
if (domain.startsWith('www.')) {
domain = domain.substring(4);
}
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// 서브도메인 차단 옵션에 따른 정규식 선택
let regexFilter;
if (isSubdomainBlockEnabled) {
// 광범위 차단 (모든 서브도메인 포함)
regexFilter = `^https?:\\/\\/(?:[^/]+\\.)*${escapedDomain}(?:\\/.*)?$`;
} else {
// 엄격 차단 (입력 도메인 및 www만)
regexFilter = `^https?:\\/\\/(?:www\\.)?${escapedDomain}(?:\\/.*)?$`;
}
return {
id: index + 1,
priority: 1,
action: {
type: 'redirect',
redirect: {
regexSubstitution: `${blockedPageUrl}?blockedUrl=\\0`,
},
},
condition: {
regexFilter: regexFilter,
resourceTypes: ['main_frame', 'sub_frame', 'xmlhttprequest'],
},
};
});
// 5. 규칙 업데이트 (기존 삭제 + 신규 추가)
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: removeRuleIds,
addRules: addRules,
});
} catch (err) { }
}
// --- 아이콘 변경 헬퍼 함수 ---
function updateExtensionIcon(isEnabled) {
if (isEnabled) {
chrome.action.setBadgeText({ text: 'ON' });
chrome.action.setBadgeBackgroundColor({ color: '#28a745' });
} else {
chrome.action.setBadgeText({ text: '' });
}
}