-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
131 lines (111 loc) · 4.6 KB
/
Copy pathbackground.js
File metadata and controls
131 lines (111 loc) · 4.6 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
// Service worker: blocks distracting sites behind the block page.
//
// Source of truth: when the Vitreous app is running it pushes Focus state over a
// loopback WebSocket and drives the extension. When the app isn't connected, the
// popup's manual toggle still works (standalone fallback).
import { isBlocked, BLOCKED_DOMAINS } from './blocklist.js'
// Cross-browser API handle. Firefox exposes the promise-based `browser` namespace;
// Chrome/Edge/Brave/Opera/Vivaldi expose promise-based `chrome` (MV3). Firefox's
// own `chrome.*` is callback-style, so `await chrome.storage…` would break there —
// shadowing the global with `browser` when present keeps every call below working.
const chrome = globalThis.browser ?? globalThis.chrome
const PORT = 8736
const KEY_ACTIVE = 'focusActive'
const KEY_DOMAINS = 'domains'
const KEY_CONNECTED = 'appConnected'
// Prompt, fixed retry so the extension reconnects within seconds of the app
// coming online — never leaving it stranded while the app is actually running.
const RECONNECT_MS = 4000
let socket = null
let reconnectTimer = null
// ---- blocking ---------------------------------------------------------------
function hostOf(url) {
try { return new URL(url).hostname } catch { return '' }
}
function blockedUrl(url) {
return `${chrome.runtime.getURL('blocked.html')}?domain=${encodeURIComponent(hostOf(url))}`
}
async function effectiveState() {
const r = await chrome.storage.local.get([KEY_ACTIVE, KEY_DOMAINS])
const domains = Array.isArray(r[KEY_DOMAINS]) && r[KEY_DOMAINS].length ? r[KEY_DOMAINS] : BLOCKED_DOMAINS
return { active: r[KEY_ACTIVE] === true, domains }
}
function reportBlock(domain) {
if (socket && socket.readyState === 1 && domain) {
try { socket.send(JSON.stringify({ t: 'web_block', domain })) } catch { /* ignore */ }
}
}
async function redirectIfBlocked(tabId, url) {
if (!tabId || !url) return
const { active, domains } = await effectiveState()
const host = hostOf(url)
if (active && isBlocked(host, domains)) {
reportBlock(host)
chrome.tabs.update(tabId, { url: blockedUrl(url) })
}
}
async function sweepOpenTabs() {
const { active, domains } = await effectiveState()
if (!active) return
const tabs = await chrome.tabs.query({})
for (const tab of tabs) {
if (tab.id && tab.url && isBlocked(hostOf(tab.url), domains)) {
reportBlock(hostOf(tab.url))
chrome.tabs.update(tab.id, { url: blockedUrl(tab.url) })
}
}
}
chrome.webNavigation.onBeforeNavigate.addListener((details) => {
if (details.frameId !== 0) return
// Blocking decisions read chrome.storage, not the socket — so we do NOT open a
// WebSocket on every navigation. (Doing so spammed the console with a
// "connection failed" error per page load whenever the app wasn't running.)
// The background reconnect timer keeps us in sync once the app is up.
redirectIfBlocked(details.tabId, details.url)
})
// Manual toggle (popup) flipping on -> sweep open tabs.
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes[KEY_ACTIVE]?.newValue === true) sweepOpenTabs()
})
// ---- app bridge (loopback WebSocket) ----------------------------------------
async function applyAppState(state) {
await chrome.storage.local.set({
[KEY_ACTIVE]: state.active === true,
[KEY_DOMAINS]: Array.isArray(state.domains) ? state.domains : [],
[KEY_CONNECTED]: true
})
if (state.active) sweepOpenTabs()
}
function connect() {
try {
socket = new WebSocket(`ws://127.0.0.1:${PORT}`)
} catch {
scheduleReconnect()
return
}
socket.onmessage = (ev) => {
try { applyAppState(JSON.parse(ev.data)) } catch { /* ignore */ }
}
socket.onclose = () => {
socket = null
// The app is the source of truth for Focus state. When it goes away, drop
// app-driven blocking so we never keep redirecting after Focus is off /
// the app has quit. (Locked & timed sessions stay enforced via the hosts
// file; the extension is only the nicer block page on top of that.)
chrome.storage.local.set({ [KEY_CONNECTED]: false, [KEY_ACTIVE]: false })
scheduleReconnect()
}
socket.onerror = () => { try { socket?.close() } catch { /* ignore */ } }
}
function ensureConnected() {
// readyState 0 = CONNECTING, 1 = OPEN
if (socket && (socket.readyState === 0 || socket.readyState === 1)) return
connect()
}
function scheduleReconnect() {
if (reconnectTimer) return
reconnectTimer = setTimeout(() => { reconnectTimer = null; ensureConnected() }, RECONNECT_MS)
}
chrome.runtime.onStartup.addListener(ensureConnected)
chrome.runtime.onInstalled.addListener(ensureConnected)
ensureConnected()