feat: continue timed breaks across devices (Closes #1038)
This commit is contained in:
1 parent
1d242bab75
commit
c5b466f281
7 files changed
+172
-33
No files matched your search
@@ -20,12 +20,13 @@ function createTodaySessionSync({
|
||||
|
||||
function adopt(session) {
|
||||
if (!session || !Number.isInteger(session.revision)) return null;
|
||||
const previousOwned = current?.device_id === deviceId() && current?.running;
|
||||
const previousOwned = current?.device_id === deviceId() &&
|
||||
(current?.running || Number.isFinite(current?.break_deadline_at));
|
||||
current = session;
|
||||
if (session.device_id === deviceId()) {
|
||||
ownedRevision = session.revision;
|
||||
onRemote(null);
|
||||
} else if (session.running && session.identity) {
|
||||
} else if ((session.running || Number.isFinite(session.break_deadline_at)) && session.identity) {
|
||||
if (previousOwned) {
|
||||
timer?.pause?.();
|
||||
onTransferred(session);
|
||||
@@ -53,15 +54,18 @@ function createTodaySessionSync({
|
||||
|
||||
function claim() {
|
||||
return serialize(async () => {
|
||||
if (!current?.running || !current.identity || current.device_id === deviceId() || !deviceId()) return null;
|
||||
if ((!current?.running && !Number.isFinite(current?.break_deadline_at)) ||
|
||||
!current.identity || current.device_id === deviceId() || !deviceId()) return null;
|
||||
try {
|
||||
onStatus('syncing');
|
||||
const body = {
|
||||
base_revision:current.revision, device_id:deviceId(), identity:current.identity,
|
||||
elapsed_ms:current.elapsed_ms, running:true,
|
||||
};
|
||||
if (Object.hasOwn(current, 'break_deadline_at')) body.break_deadline_at = null;
|
||||
const session = await fetchJson(endpoint, {
|
||||
method:'PATCH', headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
base_revision:current.revision, device_id:deviceId(), identity:current.identity,
|
||||
elapsed_ms:current.elapsed_ms, running:true,
|
||||
}),
|
||||
body:JSON.stringify(body),
|
||||
});
|
||||
adopt(session);
|
||||
timer?.adopt?.(session.identity, session.elapsed_ms, session.running);
|
||||
@@ -82,13 +86,18 @@ function createTodaySessionSync({
|
||||
if (!snapshot?.identity || !deviceId()) return null;
|
||||
try {
|
||||
onStatus('syncing');
|
||||
const body = {
|
||||
base_revision:ownedRevision, device_id:deviceId(), identity:snapshot.identity,
|
||||
elapsed_ms:Math.max(0, Math.floor(Number(snapshot.elapsed_ms) || 0)),
|
||||
running:Boolean(snapshot.running),
|
||||
};
|
||||
if (Object.hasOwn(snapshot, 'break_deadline_at') || Object.hasOwn(current || {}, 'break_deadline_at')) {
|
||||
body.break_deadline_at = Number.isFinite(snapshot.break_deadline_at) ?
|
||||
Math.floor(snapshot.break_deadline_at) : null;
|
||||
}
|
||||
const session = await fetchJson(endpoint, {
|
||||
method:'PATCH', headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
base_revision:ownedRevision, device_id:deviceId(), identity:snapshot.identity,
|
||||
elapsed_ms:Math.max(0, Math.floor(Number(snapshot.elapsed_ms) || 0)),
|
||||
running:Boolean(snapshot.running),
|
||||
}),
|
||||
body:JSON.stringify(body),
|
||||
});
|
||||
adopt(session);
|
||||
return session;
|
||||
@@ -146,6 +155,17 @@ function createTodaySessionSync({
|
||||
};
|
||||
}
|
||||
|
||||
function todaySessionHandoffSummary(session, item) {
|
||||
if (Number.isFinite(session?.break_deadline_at)) {
|
||||
const end = new Date(session.break_deadline_at).toLocaleTimeString([], {
|
||||
hour:'numeric', minute:'2-digit',
|
||||
});
|
||||
return `On break until ${end} · ready to resume here`;
|
||||
}
|
||||
const minutes = Math.max(0, Math.floor(Number(session?.elapsed_ms || 0) / 60000));
|
||||
return `${item?.title || 'Current Today item'} · ${minutes} min elapsed`;
|
||||
}
|
||||
|
||||
function attachTodaySessionHandoff({
|
||||
fetchJson, storage, timer, qs, items, identity, selectToday, startItem, announce, renderTimer,
|
||||
}) {
|
||||
@@ -176,9 +196,9 @@ function attachTodaySessionHandoff({
|
||||
handoff.hidden = !session;
|
||||
if (!session) return;
|
||||
const item = items().find(entry => identity(entry) === session.identity);
|
||||
const minutes = Math.max(0, Math.floor(Number(session.elapsed_ms || 0) / 60000));
|
||||
qs('#today-session-handoff-summary').textContent =
|
||||
`${item?.title || 'Current Today item'} · ${minutes} min elapsed`;
|
||||
qs('#today-session-handoff-summary').textContent = todaySessionHandoffSummary(session, item);
|
||||
qs('#continue-today-session').textContent = Number.isFinite(session.break_deadline_at) ?
|
||||
'Resume Today here' : 'Continue here';
|
||||
},
|
||||
onTransferred:() => {
|
||||
showSessionStatus('Session continued on another device.');
|
||||
@@ -201,5 +221,5 @@ function attachTodaySessionHandoff({
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = {
|
||||
createTodaySessionSync, attachTodaySessionHandoff,
|
||||
createTodaySessionSync, attachTodaySessionHandoff, todaySessionHandoffSummary,
|
||||
};
|
||||
+11
-4
@@ -61,10 +61,17 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
|
||||
const state = read();
|
||||
const selected = identity || state.active_identity;
|
||||
const entry = state.entries[selected];
|
||||
if (!selected || !entry) return { identity:selected, elapsed_ms:0, running:false };
|
||||
const elapsed = Math.max(0, Number(entry.elapsed_ms) || 0) + (entry.running ?
|
||||
Math.max(0, now() - Number(entry.started_at ?? now())) : 0);
|
||||
return { identity:selected, elapsed_ms:elapsed, running:Boolean(entry.running) };
|
||||
const result = !selected || !entry ?
|
||||
{ identity:selected, elapsed_ms:0, running:false } :
|
||||
{
|
||||
identity:selected,
|
||||
elapsed_ms:Math.max(0, Number(entry.elapsed_ms) || 0) + (entry.running ?
|
||||
Math.max(0, now() - Number(entry.started_at ?? now())) : 0),
|
||||
running:Boolean(entry.running),
|
||||
};
|
||||
const timedBreak = validBreak(state);
|
||||
if (timedBreak?.identity === selected) result.break_deadline_at = timedBreak.deadline_at;
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
adopt(identity, elapsedMs, running) {
|
||||
|
||||
Reference in new issue
Block a user