171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
function createTodayLockScreen({
|
|
storage,
|
|
getLogin,
|
|
serviceWorker,
|
|
NotificationRef,
|
|
control,
|
|
status,
|
|
locationRef,
|
|
historyRef,
|
|
randomToken = () => {
|
|
const bytes = new Uint8Array(16);
|
|
globalThis.crypto.getRandomValues(bytes);
|
|
return Array.from(bytes, value => value.toString(16).padStart(2, '0')).join('');
|
|
},
|
|
fingerprint = async (token, identity) => {
|
|
const input = new TextEncoder().encode(token + '\0' + identity);
|
|
const digest = await globalThis.crypto.subtle.digest('SHA-256', input);
|
|
return Array.from(new Uint8Array(digest), value =>
|
|
value.toString(16).padStart(2, '0')).join('');
|
|
},
|
|
onAction = () => {},
|
|
}) {
|
|
let activeIdentity = '';
|
|
const preferenceKey = () => {
|
|
const login = String(getLogin?.() || '').trim().toLowerCase();
|
|
return login ? 'stackchain.today-lock-screen.v1.' + encodeURIComponent(login) : '';
|
|
};
|
|
const supported = Boolean(serviceWorker && NotificationRef);
|
|
const actionKey = () => {
|
|
const login = String(getLogin?.() || '').trim().toLowerCase();
|
|
return login ? 'stackchain.today-lock-screen-action.v1.' + encodeURIComponent(login) : '';
|
|
};
|
|
const readAction = () => {
|
|
const key = actionKey();
|
|
if (!key) return null;
|
|
try {
|
|
const value = JSON.parse(storage?.getItem(key) || 'null');
|
|
return typeof value?.token === 'string' && value.token &&
|
|
typeof value.fingerprint === 'string' && value.fingerprint ? value : null;
|
|
} catch (_error) { return null; }
|
|
};
|
|
const clearAction = () => {
|
|
const key = actionKey();
|
|
try { if (key) storage?.removeItem(key); }
|
|
catch (_error) { return false; }
|
|
return true;
|
|
};
|
|
const actionFor = async identity => {
|
|
const existing = readAction();
|
|
if (existing && await fingerprint(existing.token, identity) === existing.fingerprint) return existing;
|
|
const key = actionKey();
|
|
if (!key) return null;
|
|
try {
|
|
const token = String(randomToken() || '');
|
|
const value = { token, fingerprint:token ? await fingerprint(token, identity) : '' };
|
|
if (!value.token) return null;
|
|
storage?.setItem(key, JSON.stringify(value));
|
|
return value;
|
|
} catch (_error) { return null; }
|
|
};
|
|
const enabled = () => {
|
|
const key = preferenceKey();
|
|
return Boolean(key && storage?.getItem(key) === '1');
|
|
};
|
|
const setStatus = message => {
|
|
if (status) status.textContent = message;
|
|
};
|
|
const post = async message => {
|
|
const target = serviceWorker?.controller || (await serviceWorker?.ready)?.active;
|
|
target?.postMessage?.(message);
|
|
};
|
|
const hide = () => post({
|
|
type:'stackchain-today-lock-screen', active:false, running:false,
|
|
});
|
|
const render = () => {
|
|
if (control) {
|
|
control.checked = enabled();
|
|
control.disabled = !supported;
|
|
}
|
|
if (!supported) setStatus('Lock-screen controls are not supported on this device.');
|
|
else if (enabled()) setStatus('Lock-screen Today controls are on.');
|
|
};
|
|
const consumeAction = async (action, token = '') => {
|
|
if (!enabled() || !['pause', 'resume', 'complete'].includes(action)) return false;
|
|
if (action !== 'complete') {
|
|
onAction(action, null);
|
|
return true;
|
|
}
|
|
const pending = readAction();
|
|
if (!pending || !token || !activeIdentity || pending.token !== token ||
|
|
await fingerprint(token, activeIdentity) !== pending.fingerprint) return false;
|
|
if (!clearAction()) return false;
|
|
onAction(action, activeIdentity);
|
|
return true;
|
|
};
|
|
const api = {
|
|
enabled,
|
|
async enable() {
|
|
if (!supported) {
|
|
render();
|
|
return false;
|
|
}
|
|
const permission = NotificationRef.permission === 'granted' ?
|
|
'granted' : await NotificationRef.requestPermission();
|
|
if (permission !== 'granted') {
|
|
if (control) control.checked = false;
|
|
setStatus(permission === 'denied' ?
|
|
'Lock-screen controls are blocked in browser settings.' :
|
|
'Lock-screen controls were not enabled.');
|
|
return false;
|
|
}
|
|
const key = preferenceKey();
|
|
if (!key) return false;
|
|
storage?.setItem(key, '1');
|
|
render();
|
|
return true;
|
|
},
|
|
async disable() {
|
|
const key = preferenceKey();
|
|
if (key) storage?.removeItem(key);
|
|
clearAction();
|
|
if (control) control.checked = false;
|
|
setStatus('Lock-screen Today controls are off.');
|
|
await hide();
|
|
return true;
|
|
},
|
|
async sync(snapshot, active) {
|
|
if (!enabled() || NotificationRef?.permission !== 'granted') return false;
|
|
const visible = Boolean(active && snapshot?.identity);
|
|
activeIdentity = visible ? snapshot.identity : '';
|
|
const pending = visible ? await actionFor(activeIdentity) : null;
|
|
if (!visible) clearAction();
|
|
await post({
|
|
type:'stackchain-today-lock-screen',
|
|
active:visible,
|
|
running:visible && Boolean(snapshot.running),
|
|
...(pending?.token ? { actionToken:pending.token } : {}),
|
|
});
|
|
if (visible && !pending) setStatus('Finish current is unavailable because its one-time action could not be saved.');
|
|
return true;
|
|
},
|
|
consumeAction,
|
|
async consumeLaunchAction() {
|
|
let url;
|
|
try { url = new URL(locationRef?.href || ''); }
|
|
catch (_error) { return false; }
|
|
const action = url.searchParams.get('today_timer_action');
|
|
if (!['pause', 'resume', 'complete'].includes(action)) return false;
|
|
const token = url.searchParams.get('today_action_token') || '';
|
|
url.searchParams.delete('today_timer_action');
|
|
url.searchParams.delete('today_action_token');
|
|
historyRef?.replaceState?.(null, '', url.pathname + url.search + url.hash);
|
|
return await consumeAction(action, token);
|
|
},
|
|
render,
|
|
};
|
|
control?.addEventListener?.('change', () => {
|
|
if (control.checked) api.enable();
|
|
else api.disable();
|
|
});
|
|
serviceWorker?.addEventListener?.('message', event => {
|
|
if (event.data?.type === 'stackchain-today-timer-action') {
|
|
consumeAction(String(event.data.action || ''), String(event.data.actionToken || ''));
|
|
}
|
|
});
|
|
render();
|
|
return api;
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createTodayLockScreen;
|